From 20b19494e974463311f27d9fb4f5c2b8da80ff02 Mon Sep 17 00:00:00 2001 From: Chuck Hagenbuch Date: Mon, 20 Oct 2008 21:56:33 -0400 Subject: [PATCH] initial import of incubator apps and libraries - crumb, hippo, hydra, hound, and framework/Horde_Date_Parser --- crumb/COPYING | 280 ++++++++++ crumb/LICENSE.ASL | 48 ++ crumb/LICENSE.BSDL | 24 + crumb/README | 89 +++ crumb/addclient.php | 44 ++ crumb/config/.cvsignore | 3 + crumb/config/conf.bak.php | 0 crumb/config/conf.php | 7 + crumb/config/conf.xml | 27 + crumb/config/prefs.php.dist | 21 + crumb/contactsearch.php | 37 ++ crumb/docs/CHANGES | 5 + crumb/docs/CREDITS | 24 + crumb/docs/INSTALL | 243 ++++++++ crumb/docs/RELEASE_NOTES | 49 ++ crumb/docs/TODO | 8 + crumb/index.php | 22 + crumb/lib/Block/example.php | 45 ++ crumb/lib/Crumb.php | 37 ++ crumb/lib/Driver.php | 65 +++ crumb/lib/Driver/sql.php | 181 ++++++ crumb/lib/Forms/AddClient.php | 77 +++ crumb/lib/Forms/ContactSearch.php | 36 ++ crumb/lib/base.php | 51 ++ crumb/lib/version.php | 1 + crumb/listclients.php | 23 + crumb/locale/en_US/help.xml | 14 + crumb/po/.cvsignore | 1 + crumb/po/README | 1 + crumb/scripts/sql/crumb.sql | 12 + crumb/templates/common-header.inc | 29 + crumb/templates/menu.inc | 4 + crumb/themes/screen.css | 3 + framework/Horde_Date_Parser/chronic/History.txt | 53 ++ framework/Horde_Date_Parser/chronic/README.txt | 149 +++++ framework/Horde_Date_Parser/chronic/lib/chronic.rb | 125 +++++ .../chronic/lib/chronic/chronic.rb | 239 ++++++++ .../chronic/lib/chronic/grabber.rb | 26 + .../chronic/lib/chronic/handlers.rb | 469 ++++++++++++++++ .../chronic/lib/chronic/ordinal.rb | 40 ++ .../chronic/lib/chronic/pointer.rb | 27 + .../chronic/lib/chronic/repeater.rb | 115 ++++ .../chronic/lib/chronic/repeaters/repeater_day.rb | 47 ++ .../lib/chronic/repeaters/repeater_day_name.rb | 46 ++ .../lib/chronic/repeaters/repeater_day_portion.rb | 93 ++++ .../lib/chronic/repeaters/repeater_fortnight.rb | 65 +++ .../chronic/lib/chronic/repeaters/repeater_hour.rb | 52 ++ .../lib/chronic/repeaters/repeater_minute.rb | 52 ++ .../lib/chronic/repeaters/repeater_month.rb | 61 ++ .../lib/chronic/repeaters/repeater_month_name.rb | 93 ++++ .../lib/chronic/repeaters/repeater_season.rb | 23 + .../lib/chronic/repeaters/repeater_season_name.rb | 24 + .../lib/chronic/repeaters/repeater_second.rb | 36 ++ .../chronic/lib/chronic/repeaters/repeater_time.rb | 117 ++++ .../chronic/lib/chronic/repeaters/repeater_week.rb | 68 +++ .../lib/chronic/repeaters/repeater_weekend.rb | 60 ++ .../chronic/lib/chronic/repeaters/repeater_year.rb | 58 ++ .../chronic/lib/chronic/scalar.rb | 74 +++ .../chronic/lib/chronic/separator.rb | 76 +++ .../chronic/lib/chronic/time_zone.rb | 22 + .../chronic/lib/numerizer/numerizer.rb | 103 ++++ framework/Horde_Date_Parser/chronic/test/suite.rb | 9 + .../Horde_Date_Parser/chronic/test/test_Chronic.rb | 50 ++ .../Horde_Date_Parser/chronic/test/test_Handler.rb | 110 ++++ .../chronic/test/test_Numerizer.rb | 48 ++ .../chronic/test/test_RepeaterDayName.rb | 52 ++ .../chronic/test/test_RepeaterFortnight.rb | 63 +++ .../chronic/test/test_RepeaterHour.rb | 65 +++ .../chronic/test/test_RepeaterMonth.rb | 47 ++ .../chronic/test/test_RepeaterMonthName.rb | 57 ++ .../chronic/test/test_RepeaterTime.rb | 72 +++ .../chronic/test/test_RepeaterWeek.rb | 63 +++ .../chronic/test/test_RepeaterWeekend.rb | 75 +++ .../chronic/test/test_RepeaterYear.rb | 63 +++ .../Horde_Date_Parser/chronic/test/test_Span.rb | 24 + .../Horde_Date_Parser/chronic/test/test_Time.rb | 50 ++ .../Horde_Date_Parser/chronic/test/test_Token.rb | 26 + .../Horde_Date_Parser/chronic/test/test_parsing.rb | 614 +++++++++++++++++++++ .../Horde_Date_Parser/lib/Horde/Date/Parser.php | 53 ++ .../lib/Horde/Date/Parser/Grabber.php | 26 + .../lib/Horde/Date/Parser/Handlers.php | 469 ++++++++++++++++ .../lib/Horde/Date/Parser/Ordinal.php | 40 ++ .../lib/Horde/Date/Parser/Parser.php | 239 ++++++++ .../lib/Horde/Date/Parser/Pointer.php | 27 + .../lib/Horde/Date/Parser/Repeater.php | 115 ++++ .../lib/Horde/Date/Parser/Repeaters/Day.php | 47 ++ .../lib/Horde/Date/Parser/Repeaters/DayName.php | 46 ++ .../lib/Horde/Date/Parser/Repeaters/DayPortion.php | 93 ++++ .../lib/Horde/Date/Parser/Repeaters/Fortnight.php | 65 +++ .../lib/Horde/Date/Parser/Repeaters/Hour.php | 52 ++ .../lib/Horde/Date/Parser/Repeaters/Minute.php | 52 ++ .../lib/Horde/Date/Parser/Repeaters/Month.php | 61 ++ .../lib/Horde/Date/Parser/Repeaters/MonthName.php | 93 ++++ .../lib/Horde/Date/Parser/Repeaters/Season.php | 23 + .../lib/Horde/Date/Parser/Repeaters/SeasonName.php | 24 + .../lib/Horde/Date/Parser/Repeaters/Second.php | 36 ++ .../lib/Horde/Date/Parser/Repeaters/Time.php | 117 ++++ .../lib/Horde/Date/Parser/Repeaters/Week.php | 68 +++ .../lib/Horde/Date/Parser/Repeaters/Weekend.php | 60 ++ .../lib/Horde/Date/Parser/Repeaters/Year.php | 58 ++ .../lib/Horde/Date/Parser/Scalar.php | 74 +++ .../lib/Horde/Date/Parser/Separator.php | 76 +++ .../lib/Horde/Date/Parser/TimeZone.php | 22 + framework/Horde_Date_Parser/package.xml | 0 .../test/Horde/Date/Parser/AllTests.php | 54 ++ hippo/SPEC.txt | 10 + hippo/TODO.txt | 0 hippo/planet.horde.org/.htaccess | 26 + hippo/planet.horde.org/favicon.ico | Bin 0 -> 1150 bytes hippo/planet.horde.org/libs/aggregator.php | 310 +++++++++++ hippo/planet.horde.org/libs/scripts/aggregate.php | 6 + hippo/planet.horde.org/libs/utf2entities.php | 86 +++ .../themes/planet-horde/common.xsl | 34 ++ .../themes/planet-horde/css/screen.css | 318 +++++++++++ .../themes/planet-horde/css/style.css | 37 ++ .../themes/planet-horde/img/content_corners.gif | Bin 0 -> 132 bytes .../themes/planet-horde/img/feed-icon-10x10.png | Bin 0 -> 469 bytes .../themes/planet-horde/img/logo.gif | Bin 0 -> 5503 bytes .../themes/planet-horde/img/planet-horde.psd | Bin 0 -> 359925 bytes .../themes/planet-horde/img/sidebar_bottom.gif | Bin 0 -> 1670 bytes .../themes/planet-horde/img/sidebar_head.gif | Bin 0 -> 4274 bytes .../planet.horde.org/themes/planet-horde/main.xsl | 170 ++++++ hippo/planet.sql | 134 +++++ hound/urls | 10 + hydra/lib/Page.php | 6 + hydra/lib/PageMapper.php | 6 + hydra/public/.htaccess | 6 + hydra/public/index.php | 2 + hydra/public/stylesheets/print.css | 0 hydra/public/stylesheets/screen.css | 1 + 130 files changed, 8664 insertions(+) create mode 100644 crumb/COPYING create mode 100644 crumb/LICENSE.ASL create mode 100644 crumb/LICENSE.BSDL create mode 100644 crumb/README create mode 100644 crumb/addclient.php create mode 100644 crumb/config/.cvsignore create mode 100644 crumb/config/conf.bak.php create mode 100644 crumb/config/conf.php create mode 100644 crumb/config/conf.xml create mode 100644 crumb/config/prefs.php.dist create mode 100644 crumb/contactsearch.php create mode 100644 crumb/docs/CHANGES create mode 100644 crumb/docs/CREDITS create mode 100644 crumb/docs/INSTALL create mode 100644 crumb/docs/RELEASE_NOTES create mode 100644 crumb/docs/TODO create mode 100644 crumb/index.php create mode 100644 crumb/lib/Block/example.php create mode 100644 crumb/lib/Crumb.php create mode 100644 crumb/lib/Driver.php create mode 100644 crumb/lib/Driver/sql.php create mode 100644 crumb/lib/Forms/AddClient.php create mode 100644 crumb/lib/Forms/ContactSearch.php create mode 100644 crumb/lib/base.php create mode 100644 crumb/lib/version.php create mode 100644 crumb/listclients.php create mode 100644 crumb/locale/en_US/help.xml create mode 100644 crumb/po/.cvsignore create mode 100644 crumb/po/README create mode 100644 crumb/scripts/sql/crumb.sql create mode 100644 crumb/templates/common-header.inc create mode 100644 crumb/templates/menu.inc create mode 100644 crumb/themes/screen.css create mode 100644 framework/Horde_Date_Parser/chronic/History.txt create mode 100644 framework/Horde_Date_Parser/chronic/README.txt create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/chronic.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/grabber.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/handlers.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/ordinal.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/pointer.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeater.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_name.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_portion.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_fortnight.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_hour.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_minute.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month_name.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season_name.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_second.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_time.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_week.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_weekend.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_year.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/scalar.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/separator.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/chronic/time_zone.rb create mode 100644 framework/Horde_Date_Parser/chronic/lib/numerizer/numerizer.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/suite.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_Chronic.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_Handler.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_Numerizer.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_RepeaterDayName.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_RepeaterFortnight.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_RepeaterHour.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_RepeaterMonth.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_RepeaterMonthName.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_RepeaterTime.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_RepeaterWeek.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_RepeaterWeekend.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_RepeaterYear.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_Span.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_Time.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_Token.rb create mode 100644 framework/Horde_Date_Parser/chronic/test/test_parsing.rb create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Grabber.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Handlers.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Ordinal.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Parser.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Pointer.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeater.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Day.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayName.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayPortion.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Fortnight.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Hour.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Minute.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Month.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/MonthName.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Season.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/SeasonName.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Second.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Time.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Week.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Weekend.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Year.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Scalar.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/Separator.php create mode 100644 framework/Horde_Date_Parser/lib/Horde/Date/Parser/TimeZone.php create mode 100644 framework/Horde_Date_Parser/package.xml create mode 100644 framework/Horde_Date_Parser/test/Horde/Date/Parser/AllTests.php create mode 100644 hippo/SPEC.txt create mode 100644 hippo/TODO.txt create mode 100644 hippo/planet.horde.org/.htaccess create mode 100644 hippo/planet.horde.org/favicon.ico create mode 100644 hippo/planet.horde.org/libs/aggregator.php create mode 100644 hippo/planet.horde.org/libs/scripts/aggregate.php create mode 100644 hippo/planet.horde.org/libs/utf2entities.php create mode 100644 hippo/planet.horde.org/themes/planet-horde/common.xsl create mode 100644 hippo/planet.horde.org/themes/planet-horde/css/screen.css create mode 100644 hippo/planet.horde.org/themes/planet-horde/css/style.css create mode 100644 hippo/planet.horde.org/themes/planet-horde/img/content_corners.gif create mode 100644 hippo/planet.horde.org/themes/planet-horde/img/feed-icon-10x10.png create mode 100644 hippo/planet.horde.org/themes/planet-horde/img/logo.gif create mode 100644 hippo/planet.horde.org/themes/planet-horde/img/planet-horde.psd create mode 100644 hippo/planet.horde.org/themes/planet-horde/img/sidebar_bottom.gif create mode 100644 hippo/planet.horde.org/themes/planet-horde/img/sidebar_head.gif create mode 100644 hippo/planet.horde.org/themes/planet-horde/main.xsl create mode 100644 hippo/planet.sql create mode 100644 hound/urls create mode 100644 hydra/lib/Page.php create mode 100644 hydra/lib/PageMapper.php create mode 100644 hydra/public/.htaccess create mode 100644 hydra/public/index.php create mode 100644 hydra/public/stylesheets/print.css create mode 100644 hydra/public/stylesheets/screen.css diff --git a/crumb/COPYING b/crumb/COPYING new file mode 100644 index 000000000..5a965fbc5 --- /dev/null +++ b/crumb/COPYING @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + END OF TERMS AND CONDITIONS diff --git a/crumb/LICENSE.ASL b/crumb/LICENSE.ASL new file mode 100644 index 000000000..e0b7a1365 --- /dev/null +++ b/crumb/LICENSE.ASL @@ -0,0 +1,48 @@ +Version 1.0 + +Copyright (c) 2006 The Horde Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. The end-user documentation included with the redistribution, if +any, must include the following acknowledgment: + + "This product includes software developed by the Horde Project + (http://www.horde.org/)." + +Alternately, this acknowledgment may appear in the software itself, if +and wherever such third-party acknowledgments normally appear. + +4. The names "Horde", "The Horde Project", and "Skeleton" must not be +used to endorse or promote products derived from this software without +prior written permission. For written permission, please contact +core@horde.org. + +5. Products derived from this software may not be called "Horde" or +"Skeleton", nor may "Horde" or "Skeleton" appear in their name, without +prior written permission of the Horde Project. + +THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE HORDE PROJECT OR ITS CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +This software consists of voluntary contributions made by many +individuals on behalf of the Horde Project. For more information on +the Horde Project, please see . diff --git a/crumb/LICENSE.BSDL b/crumb/LICENSE.BSDL new file mode 100644 index 000000000..e2b4fbfed --- /dev/null +++ b/crumb/LICENSE.BSDL @@ -0,0 +1,24 @@ +Copyright (c) 2006 The Horde Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HORDE PROJECT +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/crumb/README b/crumb/README new file mode 100644 index 000000000..afbe1d5d8 --- /dev/null +++ b/crumb/README @@ -0,0 +1,89 @@ +What is Crumb? +================= + +:Last update: $Date$ +:Revision: $Revision$ + +.. contents:: Contents +.. section-numbering:: + +Crumb is a light-weight CRM application. By creating the concept of a client +within Horde, Crumb allows a single place to manage functionality in +several applications with data related to a client. + +This software is OSI Certified Open Source Software. OSI Certified is a +certification mark of the `Open Source Initiative`_. + +.. _`Open Source Initiative`: http://www.opensource.org/ + + +Obtaining Crumb +------------------ + +Further information on Crumb and the latest version can be obtained at + + http://www.horde.org/crumb/ + + +Documentation +------------- + +The following documentation is available in the Crumb distribution: + +:README_: This file +:COPYING_: Copyright and license information +:LICENSE_: Copyright and license information +:`docs/CHANGES`_: Changes by release +:`docs/CREDITS`_: Project developers +:`docs/INSTALL`_: Installation instructions and notes +:`docs/TODO`_: Development TODO list +:`docs/UPGRADING`_: Pointers on upgrading from previous Crumb versions + + +Installation +------------ + +Instructions for installing Crumb can be found in the file INSTALL_ in the +``docs/`` directory of the Crumb distribution. + + +Assistance +---------- + +If you encounter problems with Crumb, help is available! + +The Horde Frequently Asked Questions List (FAQ), available on the Web at + + http://www.horde.org/faq/ + +The Horde Project runs a number of mailing lists, for individual applications +and for issues relating to the project as a whole. Information, archives, and +subscription information can be found at + + http://www.horde.org/mail/ + +Lastly, Horde developers, contributors and users also make occasional +appearances on IRC, on the channel #horde on the freenode Network +(irc.freenode.net). + + +Licensing +--------- + +For licensing and copyright information, please see the file COPYING_/LICENSE_ +in the Crumb distribution. + +Thanks, + +The Crumb team + + +.. _README: ?f=README.html +.. _COPYING: http://www.horde.org/licenses/gpl.php +.. _LICENSE: http://www.horde.org/licenses/asl.php +.. _docs/CHANGES: ?f=CHANGES.html +.. _docs/CREDITS: ?f=CREDITS.html +.. _INSTALL: +.. _docs/INSTALL: ?f=INSTALL.html +.. _docs/TODO: ?f=TODO.html +.. _docs/UPGRADING: ?f=UPGRADING.html diff --git a/crumb/addclient.php b/crumb/addclient.php new file mode 100644 index 000000000..80d4dcca5 --- /dev/null +++ b/crumb/addclient.php @@ -0,0 +1,44 @@ + + */ + +@define('CRUMB_BASE', dirname(__FILE__)); +require_once CRUMB_BASE . '/lib/base.php'; +require_once 'Horde/Variables.php'; +require_once 'Horde/Form.php'; +require_once 'Horde/Form/Renderer.php'; +require_once CRUMB_BASE . '/lib/Forms/AddClient.php'; + +$vars = Variables::getDefaultVariables(); +$formname = $vars->get('formname'); + +$addform = new Horde_Form_AddClient($vars); +if (is_a($addform, 'PEAR_Error')) { + Horde::logMessage($addform, __FILE__, __LINE__, PEAR_LOG_ERR); + $notification->push(_("An internal error has occurred. Details have been logged for the administrator.")); + $addform = null; +} + +if ($addform->validate($vars)) { +print_r($addform->getInfo()); +} + +$url = Horde::applicationUrl('addclient.php'); +$title = _("Add New Client"); + +require CRUMB_TEMPLATES . '/common-header.inc'; +require CRUMB_TEMPLATES . '/menu.inc'; + +if (!empty($addform)) { + $addform->renderActive(null, null, $url, 'post'); +} + +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/crumb/config/.cvsignore b/crumb/config/.cvsignore new file mode 100644 index 000000000..51adefac7 --- /dev/null +++ b/crumb/config/.cvsignore @@ -0,0 +1,3 @@ +conf.php +conf.bak.php +prefs.php diff --git a/crumb/config/conf.bak.php b/crumb/config/conf.bak.php new file mode 100644 index 000000000..e69de29bb diff --git a/crumb/config/conf.php b/crumb/config/conf.php new file mode 100644 index 000000000..55865e188 --- /dev/null +++ b/crumb/config/conf.php @@ -0,0 +1,7 @@ + + + + + Storage System Settings + sql + + + + crumb_clients + + + + + + + + Menu Settings + + + + + + + diff --git a/crumb/config/prefs.php.dist b/crumb/config/prefs.php.dist new file mode 100644 index 000000000..3f64c81be --- /dev/null +++ b/crumb/config/prefs.php.dist @@ -0,0 +1,21 @@ + _("Sample Prefs"), + 'label' => _("Sample Pref"), + 'desc' => _("Set your sample preference."), + 'members' => array('sample') +); + +$_prefs['sample'] = array( + 'value' => '', + 'locked' => false, + 'shared' => false, + 'type' => 'text', + 'desc' => _("This is your sample preference.") +); diff --git a/crumb/contactsearch.php b/crumb/contactsearch.php new file mode 100644 index 000000000..9acec4967 --- /dev/null +++ b/crumb/contactsearch.php @@ -0,0 +1,37 @@ + + */ + +@define('CRUMB_BASE', dirname(__FILE__)); +require_once CRUMB_BASE . '/lib/base.php'; +require_once 'Horde/Form.php'; +require_once 'Horde/Form/Renderer.php'; +require_once CRUMB_BASE . '/lib/Forms/ContactSearch.php'; +require_once 'Horde/Variables.php'; + +$vars = Variables::getDefaultVariables(); + +$searchform = new Horde_Form_ContactSearch($vars); + +$info = array(); +if ($searchform->validate($vars)) { +echo "Success!"; +} + +$url = Horde::applicationUrl(basename(__FILE__)); +$title = $searchform->getTitle(); + +require CRUMB_TEMPLATES . '/common-header.inc'; +require CRUMB_TEMPLATES . '/menu.inc'; + +$searchform->renderActive(null, null, $url, 'post'); + +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/crumb/docs/CHANGES b/crumb/docs/CHANGES new file mode 100644 index 000000000..141c0cc98 --- /dev/null +++ b/crumb/docs/CHANGES @@ -0,0 +1,5 @@ +--- +0.1 +--- + +[xyz] Initial Release diff --git a/crumb/docs/CREDITS b/crumb/docs/CREDITS new file mode 100644 index 000000000..62c26b77a --- /dev/null +++ b/crumb/docs/CREDITS @@ -0,0 +1,24 @@ +=========================== + Crumb Development Team +=========================== + + +Core Developers +=============== +Ben Klang + + +Drivers +======= + + + +Localization +============ + +===================== ====================================================== +===================== ====================================================== + + +Contributions +============= diff --git a/crumb/docs/INSTALL b/crumb/docs/INSTALL new file mode 100644 index 000000000..949162ae2 --- /dev/null +++ b/crumb/docs/INSTALL @@ -0,0 +1,243 @@ +========================= + Installing Crumb 1.0 +========================= + +:Last update: $Date: 2007/12/14 17:44:26 $ +:Revision: $Revision: 1.16 $ + +.. contents:: Contents +.. section-numbering:: + +This document contains instructions for installing the Crumb ... + +For information on the capabilities and features of Crumb, see the file +README_ in the top-level directory of the Crumb distribution. + + +Obtaining Crumb +================== + +Crumb can be obtained from the Horde website and FTP server, at + + http://www.horde.org/crumb/ + + ftp://ftp.horde.org/pub/crumb/ + +Or use the mirror closest to you: + + http://www.horde.org/mirrors.php + +Bleeding-edge development versions of Crumb are available via CVS; see the +file `horde/docs/HACKING`_ in the Horde distribution, or the website +http://www.horde.org/source/, for information on accessing the Horde CVS +repository. + + +Prerequisites +============= + +To function properly, Crumb **requires** the following: + +1. A working Horde installation. + + Crumb runs within the `Horde Application Framework`_, a set of common + tools for Web applications written in PHP. You must install Horde before + installing Crumb. + + .. Important:: Crumb 1.0 requires version 3.0+ of the Horde Framework - + earlier versions of Horde will **not** work. + + .. _`Horde Application Framework`: http://www.horde.org/horde/ + + The Horde Framework can be obtained from the Horde website and FTP server, + at + + http://www.horde.org/horde/ + + ftp://ftp.horde.org/pub/horde/ + + Many of Crumb's prerequisites are also Horde prerequisites. + + .. Important:: Be sure to have completed all of the steps in the + `horde/docs/INSTALL`_ file for the Horde Framework before + installing Crumb. + +2. The following PHP capabilities: + + a. FOO support ``--with-foo`` [OPTIONAL] + + Description of Foo and what it is used for. + +3. The following PEAR packages: + (See `horde/docs/INSTALL`_ for instructions on installing PEAR packages) + + a. PEAR_Package x.x.x [OPTIONAL] + + Crumb uses the Foo_Bar class for... + +4. The following PECL modules: + (See `horde/docs/INSTALL`_ for instructions on installing PECL modules) + + a. pecl_package x.x.x [OPTIONAL] + + pecl_package is required to... + +5. Something else. + +The following items are not required, but are strongly **recommended**: + +1. Yet something else. + + +Installing Crumb +=================== + +Crumb is written in PHP, and must be installed in a web-accessible +directory. The precise location of this directory will differ from system to +system. Conventionally, Crumb is installed directly underneath Horde in the +web server's document tree. + +Since Crumb is written in PHP, there is no compilation necessary; simply +expand the distribution where you want it to reside and rename the root +directory of the distribution to whatever you wish to appear in the URL. For +example, with the Apache web server's default document root of +``/usr/local/apache/htdocs``, you would type:: + + cd /usr/local/apache/htdocs/horde + tar zxvf /path/to/crumb-h3-x.y.z.tar.gz + mv crumb-h3-x.y.z crumb + +and would then find Crumb at the URL:: + + http://your-server/horde/crumb/ + + +Configuring Crumb +==================== + +1. Configuring Horde for Crumb + + a. Register the application + + In ``horde/config/registry.php``, find the ``applications['crumb']`` + stanza. The default settings here should be okay, but you can change + them if desired. If you have changed the location of Crumb relative + to Horde, either in the URL, in the filesystem or both, you must update + the ``fileroot`` and ``webroot`` settings to their correct values. + +2. Creating the database tables + + The specific steps to create Crumb's database tables depend on which + database you've chosen to use. + + First, look in ``scripts/sql/`` to see if a script already exists for your + database type. If so, you should be able to simply execute that script as + superuser in your database. (Note that executing the script as the "horde" + user will probably fail when granting privileges.) + + If such a script does not exist, you'll need to build your own, using the + file ``crumb.sql`` as a starting point. If you need assistance in + creating database tables, you may wish to let us know on the Crumb + mailing list. + + You will also need to make sure that the "horde" user in your database has + table-creation privileges, so that the tables that `PEAR DB`_ uses to + provide portable sequences can be created. + + .. _`PEAR DB`: http://pear.php.net/DB + +3. Configuring Crumb + + To configure Crumb, change to the ``config/`` directory of the installed + distribution, and make copies of all of the configuration ``dist`` files + without the ``dist`` suffix:: + + cd config/ + for foo in *.dist; do cp $foo `basename $foo .dist`; done + + Or on Windows:: + + copy *.dist *. + + Documentation on the format and purpose of those files can be found in each + file. You may edit these files if you wish to customize Crumb's + appearance and behavior. With one exception (``foo.php``) the defaults will + be correct for most sites. + + You must login to Horde as a Horde Administrator to finish the + configuration of Crumb. Use the Horde ``Administration`` menu item to + get to the administration page, and then click on the ``Configuration`` + icon to get the configuration page. Select ``Crumb Name`` from the + selection list of applications. Fill in or change any configuration values + as needed. When done click on ``Generate Crumb Name Configuration`` to + generate the ``conf.php`` file. If your web server doesn't have write + permissions to the Crumb configuration directory or file, it will not be + able to write the file. In this case, go back to ``Configuration`` and + choose one of the other methods to create the configuration file + ``crumb/config/conf.php``. + + Note for international users: Crumb uses GNU gettext to provide local + translations of text displayed by applications; the translations are found + in the ``po/`` directory. If a translation is not yet available for your + locale (and you wish to create one), see the ``horde/po/README`` file, or + if you're having trouble using a provided translation, please see the + `horde/docs/TRANSLATIONS`_ file for instructions. + +4. More instructions, upgrading, securing, etc. + +5. Testing Crumb + + Once you have configured Crumb, bring up the included test page in your + Web browser to ensure that all necessary prerequisites have been met. See + the `horde/docs/INSTALL`_ document for further details on Horde test + scripts. If you installed Crumb as described above, the URL to the test + page would be:: + + http://your-server/horde/crumb/test.php + + The test script will also allow you to test... + + Next, use Crumb to.... Test at least the following: + + - Foo + - Bar + + +Known Problems +============== + +... + + +Obtaining Support +================= + +If you encounter problems with Crumb, help is available! + +The Horde Frequently Asked Questions List (FAQ), available on the Web at + + http://www.horde.org/faq/ + +The Horde Project runs a number of mailing lists, for individual applications +and for issues relating to the project as a whole. Information, archives, and +subscription information can be found at + + http://www.horde.org/mail/ + +Lastly, Horde developers, contributors and users may also be found on IRC, +on the channel #horde on the Freenode Network (irc.freenode.net). + +Please keep in mind that Crumb is free software written by volunteers. +For information on reasonable support expectations, please read + + http://www.horde.org/support.php + +Thanks for using Crumb! + +The Crumb team + + +.. _README: ?f=README.html +.. _`horde/docs/HACKING`: ../../horde/docs/?f=HACKING.html +.. _`horde/docs/INSTALL`: ../../horde/docs/?f=INSTALL.html +.. _`horde/docs/TRANSLATIONS`: ../../horde/docs/?f=TRANSLATIONS.html diff --git a/crumb/docs/RELEASE_NOTES b/crumb/docs/RELEASE_NOTES new file mode 100644 index 000000000..ef85ebaff --- /dev/null +++ b/crumb/docs/RELEASE_NOTES @@ -0,0 +1,49 @@ +notes['fm']['focus'] = 4; + +/* Mailing list release notes. */ +$this->notes['ml']['changes'] = <<notes['fm']['changes'] = <<notes['name'] = 'Crumb'; +$this->notes['fm']['project'] = 'crumb'; +$this->notes['fm']['branch'] = 'Default'; diff --git a/crumb/docs/TODO b/crumb/docs/TODO new file mode 100644 index 000000000..1bc73bde0 --- /dev/null +++ b/crumb/docs/TODO @@ -0,0 +1,8 @@ +================================ + Crumb Development TODO List +================================ + +:Last update: $Date: 2007/04/22 04:51:54 $ +:Revision: $Revision: 1.2 $ + +- Example todo diff --git a/crumb/index.php b/crumb/index.php new file mode 100644 index 000000000..0a9b79e89 --- /dev/null +++ b/crumb/index.php @@ -0,0 +1,22 @@ + + */ + +@define('CRUMB_BASE', dirname(__FILE__)); +$crumb_configured = (is_readable(CRUMB_BASE . '/config/conf.php')); + +if (!$crumb_configured) { + require CRUMB_BASE . '/../lib/Test.php'; + Horde_Test::configFilesMissing('Crumb', CRUMB_BASE, + array('conf.php')); +} + +require CRUMB_BASE . '/listclients.php'; diff --git a/crumb/lib/Block/example.php b/crumb/lib/Block/example.php new file mode 100644 index 000000000..81a61601b --- /dev/null +++ b/crumb/lib/Block/example.php @@ -0,0 +1,45 @@ + array('type' => 'text', + 'name' => _("Color"), + 'default' => '#ff0000')); + } + + /** + * The title to go in this block. + * + * @return string The title text. + */ + function _title() + { + return _("Color"); + } + + /** + * The content to go in this block. + * + * @return string The content + */ + function _content() + { + $html = ''; + $html .= ''; + $html .= '
 
'; + + return sprintf($html, $this->_params['color']); + } + +} diff --git a/crumb/lib/Crumb.php b/crumb/lib/Crumb.php new file mode 100644 index 000000000..088e1416f --- /dev/null +++ b/crumb/lib/Crumb.php @@ -0,0 +1,37 @@ + + * @package Crumb + */ +class Crumb { + + /** + * Build Crumb's list of menu items. + */ + function getMenu($returnType = 'object') + { + global $conf, $registry, $browser, $print_link; + + require_once 'Horde/Menu.php'; + + $menu = new Menu(HORDE_MENU_MASK_ALL); + $menu->add(Horde::applicationUrl('listclients.php'), _("List Clients"), 'user.png', $registry->getImageDir('horde')); + $menu->add(Horde::applicationUrl('addclient.php'), _("Add Client"), 'user.png', $registry->getImageDir('horde')); + + if ($returnType == 'object') { + return $menu; + } else { + return $menu->render(); + } + } + +} diff --git a/crumb/lib/Driver.php b/crumb/lib/Driver.php new file mode 100644 index 000000000..a18b4777d --- /dev/null +++ b/crumb/lib/Driver.php @@ -0,0 +1,65 @@ + + * @package Crumb + */ +class Crumb_Driver { + + + /** + * Lists all clients. + * + * @return array Returns a list of all foos. + */ + function listClients() + { + return $this->_listClients(); + } + + /** + * Attempts to return a concrete Crumb_Driver instance based on $driver. + * + * @param string $driver The type of the concrete Crumb_Driver subclass + * to return. The class name is based on the + * storage driver ($driver). The code is + * dynamically included. + * + * @param array $params A hash containing any additional configuration + * or connection parameters a subclass might need. + * + * @return Crumb_Driver The newly created concrete Crumb_Driver + * instance, or false on an error. + */ + function factory($driver = null, $params = null) + { + if ($driver === null) { + $driver = $GLOBALS['conf']['storage']['driver']; + } + $driver = basename($driver); + + if ($params === null) { + $params = Horde::getDriverConfig('storage', $driver); + } + + $class = 'Crumb_Driver_' . $driver; + if (!class_exists($class)) { + include dirname(__FILE__) . '/Driver/' . $driver . '.php'; + } + if (class_exists($class)) { + return new $class($params); + } else { + return false; + } + } + +} diff --git a/crumb/lib/Driver/sql.php b/crumb/lib/Driver/sql.php new file mode 100644 index 000000000..c08025cb6 --- /dev/null +++ b/crumb/lib/Driver/sql.php @@ -0,0 +1,181 @@ + + * 'phptype' The database type (e.g. 'pgsql', 'mysql', etc.). + * 'table' The name of the foo table in 'database'. + * 'charset' The database's internal charset. + * + * Required by some database implementations:
+ *      'database'      The name of the database.
+ *      'hostspec'      The hostname of the database server.
+ *      'protocol'      The communication protocol ('tcp', 'unix', etc.).
+ *      'username'      The username with which to connect to the database.
+ *      'password'      The password associated with 'username'.
+ *      'options'       Additional options to pass to the database.
+ *      'tty'           The TTY on which to connect to the database.
+ *      'port'          The port on which to connect to the database.
+ * + * The table structure can be created by the scripts/sql/crumb_foo.sql + * script. + * + * $Horde$ + * + * Copyright 2007-2008 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (GPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. + * + * @author Ben Klang + * @package Crumb + */ +class Crumb_Driver_sql extends Crumb_Driver { + + /** + * Hash containing connection parameters. + * + * @var array + */ + var $_params = array(); + + /** + * Handle for the current database connection. + * + * @var DB + */ + var $_db; + + /** + * Handle for the current database connection, used for writing. Defaults + * to the same handle as $_db if a separate write database is not required. + * + * @var DB + */ + var $_write_db; + + /** + * Boolean indicating whether or not we're connected to the SQL server. + * + * @var boolean + */ + var $_connected = false; + + /** + * Constructs a new SQL storage object. + * + * @param array $params A hash containing connection parameters. + */ + function Crumb_Driver_sql($params = array()) + { + $this->_params = $params; + } + + /** + * Retrieves the list of clients from the database. + * + * @return boolean|PEAR_Error True on success, PEAR_Error on failure. + */ + function _listClients() + { + /* Make sure we have a valid database connection. */ + $this->_connect(); + + /* Build the SQL query. */ + $query = 'SELECT * FROM ' . $this->_params['table']; + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('Crumb_Driver_sql::_listClients(): %s', $query), __FILE__, __LINE__, PEAR_LOG_DEBUG); + + /* Execute the query. */ + $result = $this->_db->getAll($query, array(), DB_FETCHMODE_ASSOC); + + return $result; + } + + /** + * Attempts to open a persistent connection to the SQL server. + * + * @return boolean True on success; exits (Horde::fatal()) on error. + */ + function _connect() + { + if ($this->_connected) { + return true; + } + + Horde::assertDriverConfig($this->_params, 'storage', + array('phptype', 'charset', 'table')); + + if (!isset($this->_params['database'])) { + $this->_params['database'] = ''; + } + if (!isset($this->_params['username'])) { + $this->_params['username'] = ''; + } + if (!isset($this->_params['hostspec'])) { + $this->_params['hostspec'] = ''; + } + + /* Connect to the SQL server using the supplied parameters. */ + require_once 'DB.php'; + $this->_write_db = &DB::connect($this->_params, + array('persistent' => !empty($this->_params['persistent']))); + if (is_a($this->_write_db, 'PEAR_Error')) { + Horde::fatal($this->_write_db, __FILE__, __LINE__); + } + + // Set DB portability options. + switch ($this->_write_db->phptype) { + case 'mssql': + $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM); + break; + default: + $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS); + } + + /* Check if we need to set up the read DB connection seperately. */ + if (!empty($this->_params['splitread'])) { + $params = array_merge($this->_params, $this->_params['read']); + $this->_db = &DB::connect($params, + array('persistent' => !empty($params['persistent']))); + if (is_a($this->_db, 'PEAR_Error')) { + Horde::fatal($this->_db, __FILE__, __LINE__); + } + + // Set DB portability options. + switch ($this->_db->phptype) { + case 'mssql': + $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM); + break; + default: + $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS); + } + + } else { + /* Default to the same DB handle for the writer too. */ + $this->_db =& $this->_write_db; + } + + $this->_connected = true; + + return true; + } + + /** + * Disconnects from the SQL server and cleans up the connection. + * + * @return boolean True on success, false on failure. + */ + function _disconnect() + { + if ($this->_connected) { + $this->_connected = false; + $this->_db->disconnect(); + $this->_write_db->disconnect(); + } + + return true; + } + +} diff --git a/crumb/lib/Forms/AddClient.php b/crumb/lib/Forms/AddClient.php new file mode 100644 index 000000000..a792e05e9 --- /dev/null +++ b/crumb/lib/Forms/AddClient.php @@ -0,0 +1,77 @@ + + * + * See the enclosed file LICENSE for license information (GPL). If you + * did not receive this file, see http://www.horde.org/licenses/gpl.php. + * + * @author Ben Klang + * @package Crumb + */ +require_once 'Horde/Form/Action.php'; + +class Horde_Form_AddClient extends Horde_Form +{ + function Horde_Form_AddClient(&$vars) + { + parent::Horde_Form($vars, _("Add New Client")); + + $addOrPick = array('create' => _("Create New"), + 'assign' => _("Assign Existing")); + + $action = &Horde_Form_Action::factory('reload'); + + $select = &$this->addVariable(_("Contact Information"), 'chooseContact', 'enum', true, false, null, array($addOrPick, true)); + $select->setAction($action); + $select->setOption('trackchange', true); + + if ($vars->get('chooseContact') == 'create') { + $turbaform = $GLOBALS['registry']->call('contacts/getAddClientForm', array(&$vars)); + if (is_a($turbaform, 'PEAR_Error')) { + Horde::logMessage($addform, __FILE__, __LINE__, PEAR_LOG_ERR); + $notification->push(_("An internal error has occurred. Details have beenlogged for the administrator.")); + $addform = null; + } + $elements = $turbaform->getVariables(); + foreach ($elements as $element) { + $this->importVariable($element); + } + } elseif ($vars->get('chooseContact') == 'assign') { + require_once CRUMB_BASE . '/lib/Forms/ContactSearch.php'; + $searchform = new Horde_Form_ContactSearch($vars); + $elements = $searchform->getVariables(); + foreach ($elements as $element) { + $this->importVariable($element); + } + } + + $action = &Horde_Form_Action::factory('reload'); + $select = &$this->addVariable(_("Ticket Queue"), 'chooseQueue', 'enum', true, false, null, array($addOrPick, true)); + $select->setAction($action); + $select->setOption('trackchange', true); + + if ($vars->get('chooseQueue') == 'create') { + $whupsform = $GLOBALS['registry']->call('tickets/getAddQueueForm', array(&$vars)); + if (is_a($whupsform, 'PEAR_Error')) { + Horde::logMessage($addform, __FILE__, __LINE__, PEAR_LOG_ERR); + $notification->push(_("An internal error has occurred. Details have been logged for the administrator.")); + $addform = null; + } + $elements = $whupsform->getVariables(); + foreach ($elements as $element) { + $this->importVariable($element); + } + } elseif ($vars->get('chooseQueue') == 'assign') { + $queues = $GLOBALS['registry']->listQueues(); + + + $action = &Horde_Form_Action::factory('reload'); + $select = &$this->addVariable(_("Group"), 'rectype', 'enum', true, false, null, array($addOrPick, true)); + $select->setAction($action); + $select->setOption('trackchange', true); + + return true; + } +} diff --git a/crumb/lib/Forms/ContactSearch.php b/crumb/lib/Forms/ContactSearch.php new file mode 100644 index 000000000..50190e0e2 --- /dev/null +++ b/crumb/lib/Forms/ContactSearch.php @@ -0,0 +1,36 @@ + + * + * See the enclosed file LICENSE for license information (GPL). If you + * did not receive this file, see http://www.horde.org/licenses/gpl.php. + * + * @author Ben Klang + * @package Beatnik + */ +class Horde_Form_ContactSearch extends Horde_Form +{ + function Horde_Form_ContactSearch(&$vars) + { + parent::Horde_Form($vars, _("Search for Client Contact Record")); + + $this->addVariable(_("Name"), 'name', 'text', true, false, _("Enter a few characters to search for all clients whose names contain the search text")); + + $name = $vars->get('name'); + if (!empty($name)) { + $results = $GLOBALS['registry']->call('contacts/searchClients', array($name)); + // We only pass one search string so there is only one element at + // the top level of the results array. + $results = array_pop($results); + $contacts = array(); + foreach ($results as $contact) { + $contacts[$contact['__uid']] = $contact['name']; + } + asort($contacts); + $this->addVariable(_("Contact"), 'uid', 'radio', true, false, _("Select the matching contact record or begin a new search above"), array($contacts)); + return true; + } + } +} diff --git a/crumb/lib/base.php b/crumb/lib/base.php new file mode 100644 index 000000000..fd3c40b09 --- /dev/null +++ b/crumb/lib/base.php @@ -0,0 +1,51 @@ + + * + * This file brings in all of the dependencies that every Crumb script will + * need, and sets up objects that all scripts use. + * + * @author Ben Klang + * + */ + +// Check for a prior definition of HORDE_BASE (perhaps by an auto_prepend_file +// definition for site customization). +if (!defined('HORDE_BASE')) { + @define('HORDE_BASE', dirname(__FILE__) . '/../..'); +} + +// Load the Horde Framework core, and set up inclusion paths. +require_once HORDE_BASE . '/lib/core.php'; + +// Registry. +$registry = &Registry::singleton(); +if (is_a(($pushed = $registry->pushApp('crumb', !defined('AUTH_HANDLER'))), 'PEAR_Error')) { + if ($pushed->getCode() == 'permission_denied') { + Horde::authenticationFailureRedirect(); + } + Horde::fatal($pushed, __FILE__, __LINE__, false); +} +$conf = &$GLOBALS['conf']; +@define('CRUMB_TEMPLATES', $registry->get('templates')); + +// Notification system. +$notification = &Notification::singleton(); +$notification->attach('status'); + +// Define the base file path of Crumb. +@define('CRUMB_BASE', dirname(__FILE__) . '/..'); + +// Crumb base library +require_once CRUMB_BASE . '/lib/Crumb.php'; + +// Crumb driver +require_once CRUMB_BASE . '/lib/Driver.php'; +$crumb_driver = Crumb_Driver::factory(); + +// Start output compression. +Horde::compressOutput(); diff --git a/crumb/lib/version.php b/crumb/lib/version.php new file mode 100644 index 000000000..a283b1ce5 --- /dev/null +++ b/crumb/lib/version.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crumb/listclients.php b/crumb/listclients.php new file mode 100644 index 000000000..f7a3258d3 --- /dev/null +++ b/crumb/listclients.php @@ -0,0 +1,23 @@ + + */ + +@define('CRUMB_BASE', dirname(__FILE__)); +require_once CRUMB_BASE . '/lib/base.php'; + +$clients = $crumb_driver->listClients(); + +$title = _("List"); + +require CRUMB_TEMPLATES . '/common-header.inc'; +require CRUMB_TEMPLATES . '/menu.inc'; +print_r($clients); +require $registry->get('templates', 'horde') . '/common-footer.inc'; diff --git a/crumb/locale/en_US/help.xml b/crumb/locale/en_US/help.xml new file mode 100644 index 000000000..42bfcf393 --- /dev/null +++ b/crumb/locale/en_US/help.xml @@ -0,0 +1,14 @@ + + + + + + Skeleton Overview + + What is Skeleton? + Use this module as a blank base for Horde appliations. All of the + files in this module are to be used as examples. Please customize them + for your own application's requirements. + + + diff --git a/crumb/po/.cvsignore b/crumb/po/.cvsignore new file mode 100644 index 000000000..fd8854c89 --- /dev/null +++ b/crumb/po/.cvsignore @@ -0,0 +1 @@ +messages.po diff --git a/crumb/po/README b/crumb/po/README new file mode 100644 index 000000000..a985e94aa --- /dev/null +++ b/crumb/po/README @@ -0,0 +1 @@ +see horde/po/README diff --git a/crumb/scripts/sql/crumb.sql b/crumb/scripts/sql/crumb.sql new file mode 100644 index 000000000..24024a932 --- /dev/null +++ b/crumb/scripts/sql/crumb.sql @@ -0,0 +1,12 @@ +-- +-- $Horde$ +-- +CREATE TABLE crumb_clients ( + client_id INT NOT NULL, + turba_uid VARCHAR(255), + whups_queue INT, + group_id VARCHAR(255), + + PRIMARY KEY (client_id) +); + diff --git a/crumb/templates/common-header.inc b/crumb/templates/common-header.inc new file mode 100644 index 000000000..248520c61 --- /dev/null +++ b/crumb/templates/common-header.inc @@ -0,0 +1,29 @@ + + + + + +' : '' ?> + +get('name'); +if (!empty($title)) $page_title .= ' :: ' . $title; +if (!empty($refresh_time) && ($refresh_time > 0) && !empty($refresh_url)) { + echo "\n"; +} + +Horde::includeScriptFiles(); + +?> +<?php echo htmlspecialchars($page_title) ?> + + + + +> diff --git a/crumb/templates/menu.inc b/crumb/templates/menu.inc new file mode 100644 index 000000000..97ab6d970 --- /dev/null +++ b/crumb/templates/menu.inc @@ -0,0 +1,4 @@ + +notify(array('listeners' => 'status')) ?> diff --git a/crumb/themes/screen.css b/crumb/themes/screen.css new file mode 100644 index 000000000..c7b7e4fdd --- /dev/null +++ b/crumb/themes/screen.css @@ -0,0 +1,3 @@ +/** + * $Horde: skeleton/themes/screen.css,v 1.1 2004/11/25 02:22:34 chuck Exp $ + */ diff --git a/framework/Horde_Date_Parser/chronic/History.txt b/framework/Horde_Date_Parser/chronic/History.txt new file mode 100644 index 000000000..0f2364676 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/History.txt @@ -0,0 +1,53 @@ += 0.2.3 + +* fixed 12am/12pm (by Nicholas Schlueter) + += 0.2.2 + +* added missing files (damn you manifest) + += 0.2.1 + +* fixed time overflow issue +* implemented "next" for minute repeater +* generalized time dealiasing to dealias regardless of day portion and time position +* added additional token match for cases like "friday evening at 7" and "tomorrow evening at 7" +* added support for Time#to_s output format: "Mon Apr 02 17:00:00 PDT 2007" + += 0.2.0 2007-03-20 + +* implemented numerizer, allowing the use of number words (e.g. five weeks ago) (by shalev) + += 0.1.6 2006-01-15 + +* added 'weekend' support (by eventualbuddha) + += 0.1.5 2006-12-20 + +* fixed 'aug 20' returning next year if current month is august +* modified behavior of 'from now' +* added support for seconds on times, and thus db timestamp format: "2006-12-20 18:04:23" +* made Hoe compliant + += 0.1.4 + +* removed verbose error checking code. oops. :-/ + += 0.1.3 + +* improved regexes for word variations (by Josh Goebel) +* fixed a bug that caused "today at 3am" to return nil if current time is after 3am + += 0.1.2 + +* removed Date dependency (now works on windows properly without fiddling) + += 0.1.1 + +* run to_s on incoming object +* fixed loop loading of repeaters files (out of order on some machines) +* fixed find_within to use this instead of next (was breaking "today at 6pm") + += 0.1.0 + +* initial release \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/README.txt b/framework/Horde_Date_Parser/chronic/README.txt new file mode 100644 index 000000000..2e4f1791f --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/README.txt @@ -0,0 +1,149 @@ +Chronic + http://chronic.rubyforge.org/ + by Tom Preston-Werner + +== DESCRIPTION: + +Chronic is a natural language date/time parser written in pure Ruby. See below for the wide variety of formats Chronic will parse. + +== INSTALLATION: + +Chronic can be installed via RubyGems: + + $ sudo gem install chronic + +== USAGE: + +You can parse strings containing a natural language date using the Chronic.parse method. + + require 'rubygems' + require 'chronic' + + Time.now #=> Sun Aug 27 23:18:25 PDT 2006 + + #--- + + Chronic.parse('tomorrow') + #=> Mon Aug 28 12:00:00 PDT 2006 + + Chronic.parse('monday', :context => :past) + #=> Mon Aug 21 12:00:00 PDT 2006 + + Chronic.parse('this tuesday 5:00') + #=> Tue Aug 29 17:00:00 PDT 2006 + + Chronic.parse('this tuesday 5:00', :ambiguous_time_range => :none) + #=> Tue Aug 29 05:00:00 PDT 2006 + + Chronic.parse('may 27th', :now => Time.local(2000, 1, 1)) + #=> Sat May 27 12:00:00 PDT 2000 + + Chronic.parse('may 27th', :guess => false) + #=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007 + +See Chronic.parse for detailed usage instructions. + +== EXAMPLES: + +Chronic can parse a huge variety of date and time formats. Following is a small sample of strings that will be properly parsed. Parsing is case insensitive and will handle common abbreviations and misspellings. + +Simple + + thursday + november + summer + friday 13:00 + mon 2:35 + 4pm + 6 in the morning + friday 1pm + sat 7 in the evening + yesterday + today + tomorrow + this tuesday + next month + last winter + this morning + last night + this second + yesterday at 4:00 + last friday at 20:00 + last week tuesday + tomorrow at 6:45pm + afternoon yesterday + thursday last week + +Complex + + 3 years ago + 5 months before now + 7 hours ago + 7 days from now + 1 week hence + in 3 hours + 1 year ago tomorrow + 3 months ago saturday at 5:00 pm + 7 hours before tomorrow at noon + 3rd wednesday in november + 3rd month next year + 3rd thursday this september + 4th day last week + +Specific Dates + + January 5 + dec 25 + may 27th + October 2006 + oct 06 + jan 3 2010 + february 14, 2004 + 3 jan 2000 + 17 april 85 + 5/27/1979 + 27/5/1979 + 05/06 + 1979-05-27 + Friday + 5 + 4:00 + 17:00 + 0800 + +Specific Times (many of the above with an added time) + + January 5 at 7pm + 1979-05-27 05:00:00 + etc + +== LIMITATIONS: + +Chronic uses Ruby's built in Time class for all time storage and computation. Because of this, only times that the Time class can handle will be properly parsed. Parsing for times outside of this range will simply return nil. Support for a wider range of times is planned for a future release. + +Time zones other than the local one are not currently supported. Support for other time zones is planned for a future release. + +== LICENSE: + +(The MIT License) + +Copyright (c) 2006 Ryan Davis, Zen Spider Software + +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. diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic.rb b/framework/Horde_Date_Parser/chronic/lib/chronic.rb new file mode 100644 index 000000000..6d0e7ba76 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic.rb @@ -0,0 +1,125 @@ +#============================================================================= +# +# Name: Chronic +# Author: Tom Preston-Werner +# Purpose: Parse natural language dates and times into Time or +# Chronic::Span objects +# +#============================================================================= + +$:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed + +require 'chronic/chronic' +require 'chronic/handlers' + +require 'chronic/repeater' +require 'chronic/repeaters/repeater_year' +require 'chronic/repeaters/repeater_season' +require 'chronic/repeaters/repeater_season_name' +require 'chronic/repeaters/repeater_month' +require 'chronic/repeaters/repeater_month_name' +require 'chronic/repeaters/repeater_fortnight' +require 'chronic/repeaters/repeater_week' +require 'chronic/repeaters/repeater_weekend' +require 'chronic/repeaters/repeater_day' +require 'chronic/repeaters/repeater_day_name' +require 'chronic/repeaters/repeater_day_portion' +require 'chronic/repeaters/repeater_hour' +require 'chronic/repeaters/repeater_minute' +require 'chronic/repeaters/repeater_second' +require 'chronic/repeaters/repeater_time' + +require 'chronic/grabber' +require 'chronic/pointer' +require 'chronic/scalar' +require 'chronic/ordinal' +require 'chronic/separator' +require 'chronic/time_zone' + +require 'numerizer/numerizer' + +module Chronic + VERSION = "0.2.3" + + def self.debug; false; end +end + +alias p_orig p + +def p(val) + p_orig val + puts +end + +# class Time +# def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) +# # extra_seconds = second > 60 ? second - 60 : 0 +# # extra_minutes = minute > 59 ? minute - 59 : 0 +# # extra_hours = hour > 23 ? hour - 23 : 0 +# # extra_days = day > +# +# if month > 12 +# if month % 12 == 0 +# year += (month - 12) / 12 +# month = 12 +# else +# year += month / 12 +# month = month % 12 +# end +# end +# +# base = Time.local(year, month) +# puts base +# offset = ((day - 1) * 24 * 60 * 60) + (hour * 60 * 60) + (minute * 60) + second +# puts offset.to_s +# date = base + offset +# puts date +# date +# end +# end + +class Time + def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) + if second >= 60 + minute += second / 60 + second = second % 60 + end + + if minute >= 60 + hour += minute / 60 + minute = minute % 60 + end + + if hour >= 24 + day += hour / 24 + hour = hour % 24 + end + + # determine if there is a day overflow. this is complicated by our crappy calendar + # system (non-constant number of days per month) + day <= 56 || raise("day must be no more than 56 (makes month resolution easier)") + if day > 28 + # no month ever has fewer than 28 days, so only do this if necessary + leap_year = (year % 4 == 0) && !(year % 100 == 0) + leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + days_this_month = leap_year ? leap_year_month_days[month - 1] : common_year_month_days[month - 1] + if day > days_this_month + month += day / days_this_month + day = day % days_this_month + end + end + + if month > 12 + if month % 12 == 0 + year += (month - 12) / 12 + month = 12 + else + year += month / 12 + month = month % 12 + end + end + + Time.local(year, month, day, hour, minute, second) + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/chronic.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/chronic.rb new file mode 100644 index 000000000..5e7779f63 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/chronic.rb @@ -0,0 +1,239 @@ +module Chronic + class << self + + # Parses a string containing a natural language date or time. If the parser + # can find a date or time, either a Time or Chronic::Span will be returned + # (depending on the value of :guess). If no date or time can be found, + # +nil+ will be returned. + # + # Options are: + # + # [:context] + # :past or :future (defaults to :future) + # + # If your string represents a birthday, you can set :context to :past + # and if an ambiguous string is given, it will assume it is in the + # past. Specify :future or omit to set a future context. + # + # [:now] + # Time (defaults to Time.now) + # + # By setting :now to a Time, all computations will be based off + # of that time instead of Time.now + # + # [:guess] + # +true+ or +false+ (defaults to +true+) + # + # By default, the parser will guess a single point in time for the + # given date or time. If you'd rather have the entire time span returned, + # set :guess to +false+ and a Chronic::Span will be returned. + # + # [:ambiguous_time_range] + # Integer or :none (defaults to 6 (6am-6pm)) + # + # If an Integer is given, ambiguous times (like 5:00) will be + # assumed to be within the range of that time in the AM to that time + # in the PM. For example, if you set it to 7, then the parser will + # look for the time between 7am and 7pm. In the case of 5:00, it would + # assume that means 5:00pm. If :none is given, no assumption + # will be made, and the first matching instance of that time will + # be used. + def parse(text, specified_options = {}) + # get options and set defaults if necessary + default_options = {:context => :future, + :now => Time.now, + :guess => true, + :ambiguous_time_range => 6} + options = default_options.merge specified_options + + # ensure the specified options are valid + specified_options.keys.each do |key| + default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.") + end + [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.") + + # store now for later =) + @now = options[:now] + + # put the text into a normal format to ease scanning + text = self.pre_normalize(text) + + # get base tokens for each word + @tokens = self.base_tokenize(text) + + # scan the tokens with each token scanner + [Repeater].each do |tokenizer| + @tokens = tokenizer.scan(@tokens, options) + end + + [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer| + @tokens = tokenizer.scan(@tokens) + end + + # strip any non-tagged tokens + @tokens = @tokens.select { |token| token.tagged? } + + if Chronic.debug + puts "+---------------------------------------------------" + puts "| " + @tokens.to_s + puts "+---------------------------------------------------" + end + + # do the heavy lifting + begin + span = self.tokens_to_span(@tokens, options) + rescue + raise + return nil + end + + # guess a time within a span if required + if options[:guess] + return self.guess(span) + else + return span + end + end + + # Clean up the specified input text by stripping unwanted characters, + # converting idioms to their canonical form, converting number words + # to numbers (three => 3), and converting ordinal words to numeric + # ordinals (third => 3rd) + def pre_normalize(text) #:nodoc: + normalized_text = text.to_s.downcase + normalized_text = numericize_numbers(normalized_text) + normalized_text.gsub!(/['"\.]/, '') + normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' } + normalized_text.gsub!(/\btoday\b/, 'this day') + normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day') + normalized_text.gsub!(/\byesterday\b/, 'last day') + normalized_text.gsub!(/\bnoon\b/, '12:00') + normalized_text.gsub!(/\bmidnight\b/, '24:00') + normalized_text.gsub!(/\bbefore now\b/, 'past') + normalized_text.gsub!(/\bnow\b/, 'this second') + normalized_text.gsub!(/\b(ago|before)\b/, 'past') + normalized_text.gsub!(/\bthis past\b/, 'last') + normalized_text.gsub!(/\bthis last\b/, 'last') + normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1') + normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1') + normalized_text.gsub!(/\btonight\b/, 'this night') + normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1') + normalized_text.gsub!(/\b(hence|after|from)\b/, 'future') + normalized_text = numericize_ordinals(normalized_text) + end + + # Convert number words to numbers (three => 3) + def numericize_numbers(text) #:nodoc: + Numerizer.numerize(text) + end + + # Convert ordinal words to numeric ordinals (third => 3rd) + def numericize_ordinals(text) #:nodoc: + text + end + + # Split the text on spaces and convert each word into + # a Token + def base_tokenize(text) #:nodoc: + text.split(' ').map { |word| Token.new(word) } + end + + # Guess a specific time within the given span + def guess(span) #:nodoc: + return nil if span.nil? + if span.width > 1 + span.begin + (span.width / 2) + else + span.begin + end + end + end + + class Token #:nodoc: + attr_accessor :word, :tags + + def initialize(word) + @word = word + @tags = [] + end + + # Tag this token with the specified tag + def tag(new_tag) + @tags << new_tag + end + + # Remove all tags of the given class + def untag(tag_class) + @tags = @tags.select { |m| !m.kind_of? tag_class } + end + + # Return true if this token has any tags + def tagged? + @tags.size > 0 + end + + # Return the Tag that matches the given class + def get_tag(tag_class) + matches = @tags.select { |m| m.kind_of? tag_class } + #matches.size < 2 || raise("Multiple identical tags found") + return matches.first + end + + # Print this Token in a pretty way + def to_s + @word << '(' << @tags.join(', ') << ') ' + end + end + + # A Span represents a range of time. Since this class extends + # Range, you can use #begin and #end to get the beginning and + # ending times of the span (they will be of class Time) + class Span < Range + # Returns the width of this span in seconds + def width + (self.end - self.begin).to_i + end + + # Add a number of seconds to this span, returning the + # resulting Span + def +(seconds) + Span.new(self.begin + seconds, self.end + seconds) + end + + # Subtract a number of seconds to this span, returning the + # resulting Span + def -(seconds) + self + -seconds + end + + # Prints this span in a nice fashion + def to_s + '(' << self.begin.to_s << '..' << self.end.to_s << ')' + end + end + + # Tokens are tagged with subclassed instances of this class when + # they match specific criteria + class Tag #:nodoc: + attr_accessor :type + + def initialize(type) + @type = type + end + + def start=(s) + @now = s + end + end + + # Internal exception + class ChronicPain < Exception #:nodoc: + + end + + # This exception is raised if an invalid argument is provided to + # any of Chronic's methods + class InvalidArgumentException < Exception + + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/grabber.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/grabber.rb new file mode 100644 index 000000000..4162a260b --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/grabber.rb @@ -0,0 +1,26 @@ +#module Chronic + + class Chronic::Grabber < Chronic::Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/last/ => :last, + /this/ => :this, + /next/ => :next} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'grabber-' << @type.to_s + end + end + +#end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/handlers.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/handlers.rb new file mode 100644 index 000000000..551d632fa --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/handlers.rb @@ -0,0 +1,469 @@ +module Chronic + + class << self + + def definitions #:nodoc: + @definitions ||= + {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)], + + :date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd), + Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od), + Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy), + Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy), + Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy), + Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy), + Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd), + Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)], + + # tonight at 7pm + :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r), + Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r), + Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)], + + # 3 weeks from now, in 2 months + :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p), + Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r), + Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)], + + # 3rd week in march + :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r), + Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)] + } + end + + def tokens_to_span(tokens, options) #:nodoc: + # maybe it's a specific date + + self.definitions[:date].each do |handler| + if handler.match(tokens, self.definitions) + puts "-date" if Chronic.debug + good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # I guess it's not a specific date, maybe it's just an anchor + + self.definitions[:anchor].each do |handler| + if handler.match(tokens, self.definitions) + puts "-anchor" if Chronic.debug + good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # not an anchor, perhaps it's an arrow + + self.definitions[:arrow].each do |handler| + if handler.match(tokens, self.definitions) + puts "-arrow" if Chronic.debug + good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # not an arrow, let's hope it's a narrow + + self.definitions[:narrow].each do |handler| + if handler.match(tokens, self.definitions) + puts "-narrow" if Chronic.debug + #good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, tokens, options) + end + end + + # I guess you're out of luck! + puts "-none" if Chronic.debug + return nil + end + + #-------------- + + def day_or_time(day_start, time_tokens, options) + outer_span = Span.new(day_start, day_start + (24 * 60 * 60)) + + if !time_tokens.empty? + @now = outer_span.begin + time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options) + return time + else + return outer_span + end + end + + #-------------- + + def handle_m_d(month, day, time_tokens, options) #:nodoc: + month.start = @now + span = month.this(options[:context]) + + day_start = Time.local(span.begin.year, span.begin.month, day) + + day_or_time(day_start, time_tokens, options) + end + + def handle_rmn_sd(tokens, options) #:nodoc: + handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options) + end + + def handle_rmn_od(tokens, options) #:nodoc: + handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options) + end + + def handle_rmn_sy(tokens, options) #:nodoc: + month = tokens[0].get_tag(RepeaterMonthName).index + year = tokens[1].get_tag(ScalarYear).type + + if month == 12 + next_month_year = year + 1 + next_month_month = 1 + else + next_month_year = year + next_month_month = month + 1 + end + + begin + Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month)) + rescue ArgumentError + nil + end + end + + def handle_rdn_rmn_sd_t_tz_sy(tokens, options) #:nodoc: + month = tokens[1].get_tag(RepeaterMonthName).index + day = tokens[2].get_tag(ScalarDay).type + year = tokens[5].get_tag(ScalarYear).type + + begin + day_start = Time.local(year, month, day) + day_or_time(day_start, [tokens[3]], options) + rescue ArgumentError + nil + end + end + + def handle_rmn_sd_sy(tokens, options) #:nodoc: + month = tokens[0].get_tag(RepeaterMonthName).index + day = tokens[1].get_tag(ScalarDay).type + year = tokens[2].get_tag(ScalarYear).type + + time_tokens = tokens.last(tokens.size - 3) + + begin + day_start = Time.local(year, month, day) + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + def handle_sd_rmn_sy(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_rmn_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sm_sd_sy(tokens, options) #:nodoc: + month = tokens[0].get_tag(ScalarMonth).type + day = tokens[1].get_tag(ScalarDay).type + year = tokens[2].get_tag(ScalarYear).type + + time_tokens = tokens.last(tokens.size - 3) + + begin + day_start = Time.local(year, month, day) #:nodoc: + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + def handle_sd_sm_sy(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_sm_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sy_sm_sd(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[2], tokens[0]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_sm_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sm_sy(tokens, options) #:nodoc: + month = tokens[0].get_tag(ScalarMonth).type + year = tokens[1].get_tag(ScalarYear).type + + if month == 12 + next_month_year = year + 1 + next_month_month = 1 + else + next_month_year = year + next_month_month = month + 1 + end + + begin + Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month)) + rescue ArgumentError + nil + end + end + + # anchors + + def handle_r(tokens, options) #:nodoc: + dd_tokens = dealias_and_disambiguate_times(tokens, options) + self.get_anchor(dd_tokens, options) + end + + def handle_r_g_r(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + self.handle_r(new_tokens, options) + end + + # arrows + + def handle_srp(tokens, span, options) #:nodoc: + distance = tokens[0].get_tag(Scalar).type + repeater = tokens[1].get_tag(Repeater) + pointer = tokens[2].get_tag(Pointer).type + + repeater.offset(span, distance, pointer) + end + + def handle_s_r_p(tokens, options) #:nodoc: + repeater = tokens[1].get_tag(Repeater) + + # span = + # case true + # when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class) + # self.parse("this hour", :guess => false, :now => @now) + # when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class) + # self.parse("this minute", :guess => false, :now => @now) + # when [RepeaterMinute, RepeaterSecond].include?(repeater.class) + # self.parse("this second", :guess => false, :now => @now) + # else + # raise(ChronicPain, "Invalid repeater: #{repeater.class}") + # end + + span = self.parse("this second", :guess => false, :now => @now) + + self.handle_srp(tokens, span, options) + end + + def handle_p_s_r(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[2], tokens[0]] + self.handle_s_r_p(new_tokens, options) + end + + def handle_s_r_p_a(tokens, options) #:nodoc: + anchor_span = get_anchor(tokens[3..tokens.size - 1], options) + self.handle_srp(tokens, anchor_span, options) + end + + # narrows + + def handle_orr(tokens, outer_span, options) #:nodoc: + repeater = tokens[1].get_tag(Repeater) + repeater.start = outer_span.begin - 1 + ordinal = tokens[0].get_tag(Ordinal).type + span = nil + ordinal.times do + span = repeater.next(:future) + if span.begin > outer_span.end + span = nil + break + end + end + span + end + + def handle_o_r_s_r(tokens, options) #:nodoc: + outer_span = get_anchor([tokens[3]], options) + handle_orr(tokens[0..1], outer_span, options) + end + + def handle_o_r_g_r(tokens, options) #:nodoc: + outer_span = get_anchor(tokens[2..3], options) + handle_orr(tokens[0..1], outer_span, options) + end + + # support methods + + def get_anchor(tokens, options) #:nodoc: + grabber = Grabber.new(:this) + pointer = :future + + repeaters = self.get_repeaters(tokens) + repeaters.size.times { tokens.pop } + + if tokens.first && tokens.first.get_tag(Grabber) + grabber = tokens.first.get_tag(Grabber) + tokens.pop + end + + head = repeaters.shift + head.start = @now + + case grabber.type + when :last + outer_span = head.next(:past) + when :this + if repeaters.size > 0 + outer_span = head.this(:none) + else + outer_span = head.this(options[:context]) + end + when :next + outer_span = head.next(:future) + else raise(ChronicPain, "Invalid grabber") + end + + puts "--#{outer_span}" if Chronic.debug + anchor = find_within(repeaters, outer_span, pointer) + end + + def get_repeaters(tokens) #:nodoc: + repeaters = [] + tokens.each do |token| + if t = token.get_tag(Repeater) + repeaters << t + end + end + repeaters.sort.reverse + end + + # Recursively finds repeaters within other repeaters. + # Returns a Span representing the innermost time span + # or nil if no repeater union could be found + def find_within(tags, span, pointer) #:nodoc: + puts "--#{span}" if Chronic.debug + return span if tags.empty? + + head, *rest = tags + head.start = pointer == :future ? span.begin : span.end + h = head.this(:none) + + if span.include?(h.begin) || span.include?(h.end) + return find_within(rest, h, pointer) + else + return nil + end + end + + def dealias_and_disambiguate_times(tokens, options) #:nodoc: + # handle aliases of am/pm + # 5:00 in the morning -> 5:00 am + # 7:00 in the evening -> 7:00 pm + + day_portion_index = nil + tokens.each_with_index do |t, i| + if t.get_tag(RepeaterDayPortion) + day_portion_index = i + break + end + end + + time_index = nil + tokens.each_with_index do |t, i| + if t.get_tag(RepeaterTime) + time_index = i + break + end + end + + if (day_portion_index && time_index) + t1 = tokens[day_portion_index] + t1tag = t1.get_tag(RepeaterDayPortion) + + if [:morning].include?(t1tag.type) + puts '--morning->am' if Chronic.debug + t1.untag(RepeaterDayPortion) + t1.tag(RepeaterDayPortion.new(:am)) + elsif [:afternoon, :evening, :night].include?(t1tag.type) + puts "--#{t1tag.type}->pm" if Chronic.debug + t1.untag(RepeaterDayPortion) + t1.tag(RepeaterDayPortion.new(:pm)) + end + end + + # tokens.each_with_index do |t0, i| + # t1 = tokens[i + 1] + # if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime) + # if [:morning].include?(t1tag.type) + # puts '--morning->am' if Chronic.debug + # t1.untag(RepeaterDayPortion) + # t1.tag(RepeaterDayPortion.new(:am)) + # elsif [:afternoon, :evening, :night].include?(t1tag.type) + # puts "--#{t1tag.type}->pm" if Chronic.debug + # t1.untag(RepeaterDayPortion) + # t1.tag(RepeaterDayPortion.new(:pm)) + # end + # end + # end + + # handle ambiguous times if :ambiguous_time_range is specified + if options[:ambiguous_time_range] != :none + ttokens = [] + tokens.each_with_index do |t0, i| + ttokens << t0 + t1 = tokens[i + 1] + if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion)) + distoken = Token.new('disambiguator') + distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range])) + ttokens << distoken + end + end + tokens = ttokens + end + + tokens + end + + end + + class Handler #:nodoc: + attr_accessor :pattern, :handler_method + + def initialize(pattern, handler_method) + @pattern = pattern + @handler_method = handler_method + end + + def constantize(name) + camel = name.to_s.gsub(/(^|_)(.)/) { $2.upcase } + ::Chronic.module_eval(camel, __FILE__, __LINE__) + end + + def match(tokens, definitions) + token_index = 0 + @pattern.each do |element| + name = element.to_s + optional = name.reverse[0..0] == '?' + name = name.chop if optional + if element.instance_of? Symbol + klass = constantize(name) + match = tokens[token_index] && !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty? + return false if !match && !optional + (token_index += 1; next) if match + next if !match && optional + elsif element.instance_of? String + return true if optional && token_index == tokens.size + sub_handlers = definitions[name.intern] || raise(ChronicPain, "Invalid subset #{name} specified") + sub_handlers.each do |sub_handler| + return true if sub_handler.match(tokens[token_index..tokens.size], definitions) + end + return false + else + raise(ChronicPain, "Invalid match type: #{element.class}") + end + end + return false if token_index != tokens.size + return true + end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/ordinal.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/ordinal.rb new file mode 100644 index 000000000..45b8148e4 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/ordinal.rb @@ -0,0 +1,40 @@ +module Chronic + + class Ordinal < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_ordinals(tokens[i]) then tokens[i].tag(t) end + if t = self.scan_for_days(tokens[i]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_ordinals(token) + if token.word =~ /^(\d*)(st|nd|rd|th)$/ + return Ordinal.new($1.to_i) + end + return nil + end + + def self.scan_for_days(token) + if token.word =~ /^(\d*)(st|nd|rd|th)$/ + unless $1.to_i > 31 + return OrdinalDay.new(token.word.to_i) + end + end + return nil + end + + def to_s + 'ordinal' + end + end + + class OrdinalDay < Ordinal #:nodoc: + def to_s + super << '-day-' << @type.to_s + end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/pointer.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/pointer.rb new file mode 100644 index 000000000..224efaf96 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/pointer.rb @@ -0,0 +1,27 @@ +module Chronic + + class Pointer < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/\bpast\b/ => :past, + /\bfuture\b/ => :future, + /\bin\b/ => :future} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'pointer-' << @type.to_s + end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeater.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeater.rb new file mode 100644 index 000000000..9f80daf2f --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeater.rb @@ -0,0 +1,115 @@ +class Chronic::Repeater < Chronic::Tag #:nodoc: + def self.scan(tokens, options) + # for each token + tokens.each_index do |i| + if t = self.scan_for_month_names(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_day_names(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_day_portions(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_times(tokens[i], options) then tokens[i].tag(t); next end + if t = self.scan_for_units(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_month_names(token) + scanner = {/^jan\.?(uary)?$/ => :january, + /^feb\.?(ruary)?$/ => :february, + /^mar\.?(ch)?$/ => :march, + /^apr\.?(il)?$/ => :april, + /^may$/ => :may, + /^jun\.?e?$/ => :june, + /^jul\.?y?$/ => :july, + /^aug\.?(ust)?$/ => :august, + /^sep\.?(t\.?|tember)?$/ => :september, + /^oct\.?(ober)?$/ => :october, + /^nov\.?(ember)?$/ => :november, + /^dec\.?(ember)?$/ => :december} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterMonthName.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_day_names(token) + scanner = {/^m[ou]n(day)?$/ => :monday, + /^t(ue|eu|oo|u|)s(day)?$/ => :tuesday, + /^tue$/ => :tuesday, + /^we(dnes|nds|nns)day$/ => :wednesday, + /^wed$/ => :wednesday, + /^th(urs|ers)day$/ => :thursday, + /^thu$/ => :thursday, + /^fr[iy](day)?$/ => :friday, + /^sat(t?[ue]rday)?$/ => :saturday, + /^su[nm](day)?$/ => :sunday} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterDayName.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_day_portions(token) + scanner = {/^ams?$/ => :am, + /^pms?$/ => :pm, + /^mornings?$/ => :morning, + /^afternoons?$/ => :afternoon, + /^evenings?$/ => :evening, + /^(night|nite)s?$/ => :night} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterDayPortion.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_times(token, options) + if token.word =~ /^\d{1,2}(:?\d{2})?([\.:]?\d{2})?$/ + return Chronic::RepeaterTime.new(token.word, options) + end + return nil + end + + def self.scan_for_units(token) + scanner = {/^years?$/ => :year, + /^seasons?$/ => :season, + /^months?$/ => :month, + /^fortnights?$/ => :fortnight, + /^weeks?$/ => :week, + /^weekends?$/ => :weekend, + /^days?$/ => :day, + /^hours?$/ => :hour, + /^minutes?$/ => :minute, + /^seconds?$/ => :second} + scanner.keys.each do |scanner_item| + if scanner_item =~ token.word + klass_name = 'Chronic::Repeater' + scanner[scanner_item].to_s.capitalize + klass = eval(klass_name) + return klass.new(scanner[scanner_item]) + end + end + return nil + end + + def <=>(other) + width <=> other.width + end + + # returns the width (in seconds or months) of this repeatable. + def width + raise("Repeatable#width must be overridden in subclasses") + end + + # returns the next occurance of this repeatable. + def next(pointer) + !@now.nil? || raise("Start point must be set before calling #next") + [:future, :none, :past].include?(pointer) || raise("First argument 'pointer' must be one of :past or :future") + #raise("Repeatable#next must be overridden in subclasses") + end + + def this(pointer) + !@now.nil? || raise("Start point must be set before calling #this") + [:future, :past, :none].include?(pointer) || raise("First argument 'pointer' must be one of :past, :future, :none") + end + + def to_s + 'repeater' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day.rb new file mode 100644 index 000000000..a92d83f63 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day.rb @@ -0,0 +1,47 @@ +class Chronic::RepeaterDay < Chronic::Repeater #:nodoc: + DAY_SECONDS = 86_400 # (24 * 60 * 60) + + def next(pointer) + super + + if !@current_day_start + @current_day_start = Time.local(@now.year, @now.month, @now.day) + end + + direction = pointer == :future ? 1 : -1 + @current_day_start += direction * DAY_SECONDS + + Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + day_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS + when :past + day_begin = Time.construct(@now.year, @now.month, @now.day) + day_end = Time.construct(@now.year, @now.month, @now.day, @now.hour) + when :none + day_begin = Time.construct(@now.year, @now.month, @now.day) + day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS + end + + Chronic::Span.new(day_begin, day_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * DAY_SECONDS + end + + def width + DAY_SECONDS + end + + def to_s + super << '-day' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_name.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_name.rb new file mode 100644 index 000000000..0486a4ddf --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_name.rb @@ -0,0 +1,46 @@ +class Chronic::RepeaterDayName < Chronic::Repeater #:nodoc: + DAY_SECONDS = 86400 # (24 * 60 * 60) + + def next(pointer) + super + + direction = pointer == :future ? 1 : -1 + + if !@current_day_start + @current_day_start = Time.construct(@now.year, @now.month, @now.day) + @current_day_start += direction * DAY_SECONDS + + day_num = symbol_to_number(@type) + + while @current_day_start.wday != day_num + @current_day_start += direction * DAY_SECONDS + end + else + @current_day_start += direction * 7 * DAY_SECONDS + end + + Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS) + end + + def this(pointer = :future) + super + + pointer = :future if pointer == :none + self.next(pointer) + end + + def width + DAY_SECONDS + end + + def to_s + super << '-dayname-' << @type.to_s + end + + private + + def symbol_to_number(sym) + lookup = {:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6} + lookup[sym] || raise("Invalid symbol specified") + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_portion.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_portion.rb new file mode 100644 index 000000000..c854933ad --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_portion.rb @@ -0,0 +1,93 @@ +class Chronic::RepeaterDayPortion < Chronic::Repeater #:nodoc: + @@morning = (6 * 60 * 60)..(12 * 60 * 60) # 6am-12am + @@afternoon = (13 * 60 * 60)..(17 * 60 * 60) # 1pm-5pm + @@evening = (17 * 60 * 60)..(20 * 60 * 60) # 5pm-8pm + @@night = (20 * 60 * 60)..(24 * 60 * 60) # 8pm-12pm + + def initialize(type) + super + + if type.kind_of? Integer + @range = (@type * 60 * 60)..((@type + 12) * 60 * 60) + else + lookup = {:am => 0..(12 * 60 * 60 - 1), + :pm => (12 * 60 * 60)..(24 * 60 * 60 - 1), + :morning => @@morning, + :afternoon => @@afternoon, + :evening => @@evening, + :night => @@night} + @range = lookup[type] + lookup[type] || raise("Invalid type '#{type}' for RepeaterDayPortion") + end + @range || raise("Range should have been set by now") + end + + def next(pointer) + super + + full_day = 60 * 60 * 24 + + if !@current_span + now_seconds = @now - Time.construct(@now.year, @now.month, @now.day) + if now_seconds < @range.begin + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin + end + elsif now_seconds > @range.end + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + end + else + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin + end + end + + @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin)) + else + case pointer + when :future + @current_span += full_day + when :past + @current_span -= full_day + end + end + end + + def this(context = :future) + super + + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin)) + end + + def offset(span, amount, pointer) + @now = span.begin + portion_span = self.next(pointer) + direction = pointer == :future ? 1 : -1 + portion_span + (direction * (amount - 1) * Chronic::RepeaterDay::DAY_SECONDS) + end + + def width + @range || raise("Range has not been set") + return @current_span.width if @current_span + if @type.kind_of? Integer + return (12 * 60 * 60) + else + @range.end - @range.begin + end + end + + def to_s + super << '-dayportion-' << @type.to_s + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_fortnight.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_fortnight.rb new file mode 100644 index 000000000..058fbb904 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_fortnight.rb @@ -0,0 +1,65 @@ +class Chronic::RepeaterFortnight < Chronic::Repeater #:nodoc: + FORTNIGHT_SECONDS = 1_209_600 # (14 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_fortnight_start + case pointer + when :future + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + next_sunday_span = sunday_repeater.next(:future) + @current_fortnight_start = next_sunday_span.begin + when :past + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS) + 2.times { sunday_repeater.next(:past) } + last_sunday_span = sunday_repeater.next(:past) + @current_fortnight_start = last_sunday_span.begin + end + else + direction = pointer == :future ? 1 : -1 + @current_fortnight_start += direction * FORTNIGHT_SECONDS + end + + Chronic::Span.new(@current_fortnight_start, @current_fortnight_start + FORTNIGHT_SECONDS) + end + + def this(pointer = :future) + super + + pointer = :future if pointer == :none + + case pointer + when :future + this_fortnight_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + sunday_repeater.this(:future) + this_sunday_span = sunday_repeater.this(:future) + this_fortnight_end = this_sunday_span.begin + Chronic::Span.new(this_fortnight_start, this_fortnight_end) + when :past + this_fortnight_end = Time.construct(@now.year, @now.month, @now.day, @now.hour) + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_fortnight_start = last_sunday_span.begin + Chronic::Span.new(this_fortnight_start, this_fortnight_end) + end + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * FORTNIGHT_SECONDS + end + + def width + FORTNIGHT_SECONDS + end + + def to_s + super << '-fortnight' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_hour.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_hour.rb new file mode 100644 index 000000000..f38a3f825 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_hour.rb @@ -0,0 +1,52 @@ +class Chronic::RepeaterHour < Chronic::Repeater #:nodoc: + HOUR_SECONDS = 3600 # 60 * 60 + + def next(pointer) + super + + if !@current_hour_start + case pointer + when :future + @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + when :past + @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour - 1) + end + else + direction = pointer == :future ? 1 : -1 + @current_hour_start += direction * HOUR_SECONDS + end + + Chronic::Span.new(@current_hour_start, @current_hour_start + HOUR_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1) + hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + when :past + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + when :none + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + hour_end = hour_begin + HOUR_SECONDS + end + + Chronic::Span.new(hour_start, hour_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * HOUR_SECONDS + end + + def width + HOUR_SECONDS + end + + def to_s + super << '-hour' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_minute.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_minute.rb new file mode 100644 index 000000000..342d3cd41 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_minute.rb @@ -0,0 +1,52 @@ +class Chronic::RepeaterMinute < Chronic::Repeater #:nodoc: + MINUTE_SECONDS = 60 + + def next(pointer = :future) + super + + if !@current_minute_start + case pointer + when :future + @current_minute_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1) + when :past + @current_minute_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min - 1) + end + else + direction = pointer == :future ? 1 : -1 + @current_minute_start += direction * MINUTE_SECONDS + end + + Chronic::Span.new(@current_minute_start, @current_minute_start + MINUTE_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + minute_begin = @now + minute_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + when :past + minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + minute_end = @now + when :none + minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + minute_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + MINUTE_SECONDS + end + + Chronic::Span.new(minute_begin, minute_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * MINUTE_SECONDS + end + + def width + MINUTE_SECONDS + end + + def to_s + super << '-minute' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month.rb new file mode 100644 index 000000000..edd89eeb2 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month.rb @@ -0,0 +1,61 @@ +class Chronic::RepeaterMonth < Chronic::Repeater #:nodoc: + MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60 + YEAR_MONTHS = 12 + + def next(pointer) + super + + if !@current_month_start + @current_month_start = offset_by(Time.construct(@now.year, @now.month), 1, pointer) + else + @current_month_start = offset_by(Time.construct(@current_month_start.year, @current_month_start.month), 1, pointer) + end + + Chronic::Span.new(@current_month_start, Time.construct(@current_month_start.year, @current_month_start.month + 1)) + end + + def this(pointer = :future) + super + + case pointer + when :future + month_start = Time.construct(@now.year, @now.month, @now.day + 1) + month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future) + when :past + month_start = Time.construct(@now.year, @now.month) + month_end = Time.construct(@now.year, @now.month, @now.day) + when :none + month_start = Time.construct(@now.year, @now.month) + month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future) + end + + Chronic::Span.new(month_start, month_end) + end + + def offset(span, amount, pointer) + Chronic::Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer)) + end + + def offset_by(time, amount, pointer) + direction = pointer == :future ? 1 : -1 + + amount_years = direction * amount / YEAR_MONTHS + amount_months = direction * amount % YEAR_MONTHS + + new_year = time.year + amount_years + new_month = time.month + amount_months + if new_month > YEAR_MONTHS + new_year += 1 + new_month -= YEAR_MONTHS + end + Time.construct(new_year, new_month, time.day, time.hour, time.min, time.sec) + end + + def width + MONTH_SECONDS + end + + def to_s + super << '-month' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month_name.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month_name.rb new file mode 100644 index 000000000..1f8b748a9 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month_name.rb @@ -0,0 +1,93 @@ +class Chronic::RepeaterMonthName < Chronic::Repeater #:nodoc: + MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60 + + def next(pointer) + super + + if !@current_month_begin + target_month = symbol_to_number(@type) + case pointer + when :future + if @now.month < target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month > target_month + @current_month_begin = Time.construct(@now.year + 1, target_month) + end + when :none + if @now.month <= target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month > target_month + @current_month_begin = Time.construct(@now.year + 1, target_month) + end + when :past + if @now.month > target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month < target_month + @current_month_begin = Time.construct(@now.year - 1, target_month) + end + end + @current_month_begin || raise("Current month should be set by now") + else + case pointer + when :future + @current_month_begin = Time.construct(@current_month_begin.year + 1, @current_month_begin.month) + when :past + @current_month_begin = Time.construct(@current_month_begin.year - 1, @current_month_begin.month) + end + end + + cur_month_year = @current_month_begin.year + cur_month_month = @current_month_begin.month + + if cur_month_month == 12 + next_month_year = cur_month_year + 1 + next_month_month = 1 + else + next_month_year = cur_month_year + next_month_month = cur_month_month + 1 + end + + Chronic::Span.new(@current_month_begin, Time.construct(next_month_year, next_month_month)) + end + + def this(pointer = :future) + super + + case pointer + when :past + self.next(pointer) + when :future, :none + self.next(:none) + end + end + + def width + MONTH_SECONDS + end + + def index + symbol_to_number(@type) + end + + def to_s + super << '-monthname-' << @type.to_s + end + + private + + def symbol_to_number(sym) + lookup = {:january => 1, + :february => 2, + :march => 3, + :april => 4, + :may => 5, + :june => 6, + :july => 7, + :august => 8, + :september => 9, + :october => 10, + :november => 11, + :december => 12} + lookup[sym] || raise("Invalid symbol specified") + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season.rb new file mode 100644 index 000000000..a255865fb --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season.rb @@ -0,0 +1,23 @@ +class Chronic::RepeaterSeason < Chronic::Repeater #:nodoc: + SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60 + + def next(pointer) + super + + raise 'Not implemented' + end + + def this(pointer = :future) + super + + raise 'Not implemented' + end + + def width + SEASON_SECONDS + end + + def to_s + super << '-season' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season_name.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season_name.rb new file mode 100644 index 000000000..adfd1f281 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season_name.rb @@ -0,0 +1,24 @@ +class Chronic::RepeaterSeasonName < Chronic::RepeaterSeason #:nodoc: + @summer = ['jul 21', 'sep 22'] + @autumn = ['sep 23', 'dec 21'] + @winter = ['dec 22', 'mar 19'] + @spring = ['mar 20', 'jul 20'] + + def next(pointer) + super + raise 'Not implemented' + end + + def this(pointer = :future) + super + raise 'Not implemented' + end + + def width + (91 * 24 * 60 * 60) + end + + def to_s + super << '-season-' << @type.to_s + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_second.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_second.rb new file mode 100644 index 000000000..6d05545ca --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_second.rb @@ -0,0 +1,36 @@ +class Chronic::RepeaterSecond < Chronic::Repeater #:nodoc: + SECOND_SECONDS = 1 # haha, awesome + + def next(pointer = :future) + super + + direction = pointer == :future ? 1 : -1 + + if !@second_start + @second_start = @now + (direction * SECOND_SECONDS) + else + @second_start += SECOND_SECONDS * direction + end + + Chronic::Span.new(@second_start, @second_start + SECOND_SECONDS) + end + + def this(pointer = :future) + super + + Chronic::Span.new(@now, @now + 1) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * SECOND_SECONDS + end + + def width + SECOND_SECONDS + end + + def to_s + super << '-second' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_time.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_time.rb new file mode 100644 index 000000000..f8560141c --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_time.rb @@ -0,0 +1,117 @@ +class Chronic::RepeaterTime < Chronic::Repeater #:nodoc: + class Tick #:nodoc: + attr_accessor :time + + def initialize(time, ambiguous = false) + @time = time + @ambiguous = ambiguous + end + + def ambiguous? + @ambiguous + end + + def *(other) + Tick.new(@time * other, @ambiguous) + end + + def to_f + @time.to_f + end + + def to_s + @time.to_s + (@ambiguous ? '?' : '') + end + end + + def initialize(time, options = {}) + t = time.gsub(/\:/, '') + @type = + if (1..2) === t.size + hours = t.to_i + hours == 12 ? Tick.new(0 * 60 * 60, true) : Tick.new(hours * 60 * 60, true) + elsif t.size == 3 + Tick.new((t[0..0].to_i * 60 * 60) + (t[1..2].to_i * 60), true) + elsif t.size == 4 + ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12 + hours = t[0..1].to_i + hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60, ambiguous) + elsif t.size == 5 + Tick.new(t[0..0].to_i * 60 * 60 + t[1..2].to_i * 60 + t[3..4].to_i, true) + elsif t.size == 6 + ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12 + hours = t[0..1].to_i + hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous) + else + raise("Time cannot exceed six digits") + end + end + + # Return the next past or future Span for the time that this Repeater represents + # pointer - Symbol representing which temporal direction to fetch the next day + # must be either :past or :future + def next(pointer) + super + + half_day = 60 * 60 * 12 + full_day = 60 * 60 * 24 + + first = false + + unless @current_time + first = true + midnight = Time.local(@now.year, @now.month, @now.day) + yesterday_midnight = midnight - full_day + tomorrow_midnight = midnight + full_day + + catch :done do + if pointer == :future + if @type.ambiguous? + [midnight + @type, midnight + half_day + @type, tomorrow_midnight + @type].each do |t| + (@current_time = t; throw :done) if t >= @now + end + else + [midnight + @type, tomorrow_midnight + @type].each do |t| + (@current_time = t; throw :done) if t >= @now + end + end + else # pointer == :past + if @type.ambiguous? + [midnight + half_day + @type, midnight + @type, yesterday_midnight + @type * 2].each do |t| + (@current_time = t; throw :done) if t <= @now + end + else + [midnight + @type, yesterday_midnight + @type].each do |t| + (@current_time = t; throw :done) if t <= @now + end + end + end + end + + @current_time || raise("Current time cannot be nil at this point") + end + + unless first + increment = @type.ambiguous? ? half_day : full_day + @current_time += pointer == :future ? increment : -increment + end + + Chronic::Span.new(@current_time, @current_time + width) + end + + def this(context = :future) + super + + context = :future if context == :none + + self.next(context) + end + + def width + 1 + end + + def to_s + super << '-time-' << @type.to_s + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_week.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_week.rb new file mode 100644 index 000000000..ec88ff14b --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_week.rb @@ -0,0 +1,68 @@ +class Chronic::RepeaterWeek < Chronic::Repeater #:nodoc: + WEEK_SECONDS = 604800 # (7 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_week_start + case pointer + when :future + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + next_sunday_span = sunday_repeater.next(:future) + @current_week_start = next_sunday_span.begin + when :past + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS) + sunday_repeater.next(:past) + last_sunday_span = sunday_repeater.next(:past) + @current_week_start = last_sunday_span.begin + end + else + direction = pointer == :future ? 1 : -1 + @current_week_start += direction * WEEK_SECONDS + end + + Chronic::Span.new(@current_week_start, @current_week_start + WEEK_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + this_week_start = Time.local(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + this_sunday_span = sunday_repeater.this(:future) + this_week_end = this_sunday_span.begin + Chronic::Span.new(this_week_start, this_week_end) + when :past + this_week_end = Time.local(@now.year, @now.month, @now.day, @now.hour) + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_week_start = last_sunday_span.begin + Chronic::Span.new(this_week_start, this_week_end) + when :none + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_week_start = last_sunday_span.begin + Chronic::Span.new(this_week_start, this_week_start + WEEK_SECONDS) + end + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * WEEK_SECONDS + end + + def width + WEEK_SECONDS + end + + def to_s + super << '-week' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_weekend.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_weekend.rb new file mode 100644 index 000000000..f012267d9 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_weekend.rb @@ -0,0 +1,60 @@ +class Chronic::RepeaterWeekend < Chronic::Repeater #:nodoc: + WEEKEND_SECONDS = 172_800 # (2 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_week_start + case pointer + when :future + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + next_saturday_span = saturday_repeater.next(:future) + @current_week_start = next_saturday_span.begin + when :past + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS) + last_saturday_span = saturday_repeater.next(:past) + @current_week_start = last_saturday_span.begin + end + else + direction = pointer == :future ? 1 : -1 + @current_week_start += direction * Chronic::RepeaterWeek::WEEK_SECONDS + end + + Chronic::Span.new(@current_week_start, @current_week_start + WEEKEND_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future, :none + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + this_saturday_span = saturday_repeater.this(:future) + Chronic::Span.new(this_saturday_span.begin, this_saturday_span.begin + WEEKEND_SECONDS) + when :past + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + last_saturday_span = saturday_repeater.this(:past) + Chronic::Span.new(last_saturday_span.begin, last_saturday_span.begin + WEEKEND_SECONDS) + end + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = span.begin + start = weekend.next(pointer).begin + (amount - 1) * direction * Chronic::RepeaterWeek::WEEK_SECONDS + Chronic::Span.new(start, start + (span.end - span.begin)) + end + + def width + WEEKEND_SECONDS + end + + def to_s + super << '-weekend' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_year.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_year.rb new file mode 100644 index 000000000..426371f9b --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_year.rb @@ -0,0 +1,58 @@ +class Chronic::RepeaterYear < Chronic::Repeater #:nodoc: + + def next(pointer) + super + + if !@current_year_start + case pointer + when :future + @current_year_start = Time.construct(@now.year + 1) + when :past + @current_year_start = Time.construct(@now.year - 1) + end + else + diff = pointer == :future ? 1 : -1 + @current_year_start = Time.construct(@current_year_start.year + diff) + end + + Chronic::Span.new(@current_year_start, Time.construct(@current_year_start.year + 1)) + end + + def this(pointer = :future) + super + + case pointer + when :future + this_year_start = Time.construct(@now.year, @now.month, @now.day) + Chronic::RepeaterDay::DAY_SECONDS + this_year_end = Time.construct(@now.year + 1, 1, 1) + when :past + this_year_start = Time.construct(@now.year, 1, 1) + this_year_end = Time.construct(@now.year, @now.month, @now.day) + when :none + this_year_start = Time.construct(@now.year, 1, 1) + this_year_end = Time.construct(@now.year + 1, 1, 1) + end + + Chronic::Span.new(this_year_start, this_year_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + + sb = span.begin + new_begin = Time.construct(sb.year + (amount * direction), sb.month, sb.day, sb.hour, sb.min, sb.sec) + + se = span.end + new_end = Time.construct(se.year + (amount * direction), se.month, se.day, se.hour, se.min, se.sec) + + Chronic::Span.new(new_begin, new_end) + end + + def width + (365 * 24 * 60 * 60) + end + + def to_s + super << '-year' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/scalar.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/scalar.rb new file mode 100644 index 000000000..b08cfee18 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/scalar.rb @@ -0,0 +1,74 @@ +module Chronic + + class Scalar < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_scalars(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_days(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_months(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_years(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_scalars(token, post_token) + if token.word =~ /^\d*$/ + unless post_token && %w{am pm morning afternoon evening night}.include?(post_token) + return Scalar.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_days(token, post_token) + if token.word =~ /^\d\d?$/ + unless token.word.to_i > 31 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token)) + return ScalarDay.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_months(token, post_token) + if token.word =~ /^\d\d?$/ + unless token.word.to_i > 12 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token)) + return ScalarMonth.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_years(token, post_token) + if token.word =~ /^([1-9]\d)?\d\d?$/ + unless post_token && %w{am pm morning afternoon evening night}.include?(post_token) + return ScalarYear.new(token.word.to_i) + end + end + return nil + end + + def to_s + 'scalar' + end + end + + class ScalarDay < Scalar #:nodoc: + def to_s + super << '-day-' << @type.to_s + end + end + + class ScalarMonth < Scalar #:nodoc: + def to_s + super << '-month-' << @type.to_s + end + end + + class ScalarYear < Scalar #:nodoc: + def to_s + super << '-year-' << @type.to_s + end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/separator.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/separator.rb new file mode 100644 index 000000000..86c56e33b --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/separator.rb @@ -0,0 +1,76 @@ +module Chronic + + class Separator < Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_commas(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_slash_or_dash(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_at(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_in(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_commas(token) + scanner = {/^,$/ => :comma} + scanner.keys.each do |scanner_item| + return SeparatorComma.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_slash_or_dash(token) + scanner = {/^-$/ => :dash, + /^\/$/ => :slash} + scanner.keys.each do |scanner_item| + return SeparatorSlashOrDash.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_at(token) + scanner = {/^(at|@)$/ => :at} + scanner.keys.each do |scanner_item| + return SeparatorAt.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_in(token) + scanner = {/^in$/ => :in} + scanner.keys.each do |scanner_item| + return SeparatorIn.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'separator' + end + end + + class SeparatorComma < Separator #:nodoc: + def to_s + super << '-comma' + end + end + + class SeparatorSlashOrDash < Separator #:nodoc: + def to_s + super << '-slashordash-' << @type.to_s + end + end + + class SeparatorAt < Separator #:nodoc: + def to_s + super << '-at' + end + end + + class SeparatorIn < Separator #:nodoc: + def to_s + super << '-in' + end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/time_zone.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/time_zone.rb new file mode 100644 index 000000000..41041ef47 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/chronic/time_zone.rb @@ -0,0 +1,22 @@ +module Chronic + class TimeZone < Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/[PMCE][DS]T/i => :tz} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'timezone' + end + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/lib/numerizer/numerizer.rb b/framework/Horde_Date_Parser/chronic/lib/numerizer/numerizer.rb new file mode 100644 index 000000000..8b02e6260 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/lib/numerizer/numerizer.rb @@ -0,0 +1,103 @@ +require 'strscan' + +class Numerizer + + DIRECT_NUMS = [ + ['eleven', '11'], + ['twelve', '12'], + ['thirteen', '13'], + ['fourteen', '14'], + ['fifteen', '15'], + ['sixteen', '16'], + ['seventeen', '17'], + ['eighteen', '18'], + ['nineteen', '19'], + ['ninteen', '19'], # Common mis-spelling + ['zero', '0'], + ['one', '1'], + ['two', '2'], + ['three', '3'], + ['four(\W|$)', '4\1'], # The weird regex is so that it matches four but not fourty + ['five', '5'], + ['six(\W|$)', '6\1'], + ['seven(\W|$)', '7\1'], + ['eight(\W|$)', '8\1'], + ['nine(\W|$)', '9\1'], + ['ten', '10'], + ['\ba[\b^$]', '1'] # doesn't make sense for an 'a' at the end to be a 1 + ] + + TEN_PREFIXES = [ ['twenty', 20], + ['thirty', 30], + ['fourty', 40], + ['fifty', 50], + ['sixty', 60], + ['seventy', 70], + ['eighty', 80], + ['ninety', 90] + ] + + BIG_PREFIXES = [ ['hundred', 100], + ['thousand', 1000], + ['million', 1_000_000], + ['billion', 1_000_000_000], + ['trillion', 1_000_000_000_000], + ] + +class << self + def numerize(string) + string = string.dup + + # preprocess + string.gsub!(/ +|([^\d])-([^d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction + string.gsub!(/a half/, 'haAlf') # take the 'a' out so it doesn't turn into a 1, save the half for the end + + # easy/direct replacements + + DIRECT_NUMS.each do |dn| + string.gsub!(/#{dn[0]}/i, dn[1]) + end + + # ten, twenty, etc. + + TEN_PREFIXES.each do |tp| + string.gsub!(/(?:#{tp[0]})( *\d(?=[^\d]|$))*/i) { (tp[1] + $1.to_i).to_s } + end + + # hundreds, thousands, millions, etc. + + BIG_PREFIXES.each do |bp| + string.gsub!(/(\d*) *#{bp[0]}/i) { (bp[1] * $1.to_i).to_s} + andition(string) + #combine_numbers(string) # Should to be more efficient way to do this + end + + # fractional addition + # I'm not combining this with the previous block as using float addition complicates the strings + # (with extraneous .0's and such ) + string.gsub!(/(\d+)(?: | and |-)*haAlf/i) { ($1.to_f + 0.5).to_s } + + string + end + +private + def andition(string) + sc = StringScanner.new(string) + while(sc.scan_until(/(\d+)( | and )(\d+)(?=[^\w]|$)/i)) + if sc[2] =~ /and/ || sc[1].size > sc[3].size + string[(sc.pos - sc.matched_size)..(sc.pos-1)] = (sc[1].to_i + sc[3].to_i).to_s + sc.reset + end + end + end + +# def combine_numbers(string) +# sc = StringScanner.new(string) +# while(sc.scan_until(/(\d+)(?: | and |-)(\d+)(?=[^\w]|$)/i)) +# string[(sc.pos - sc.matched_size)..(sc.pos-1)] = (sc[1].to_i + sc[2].to_i).to_s +# sc.reset +# end +# end + +end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/suite.rb b/framework/Horde_Date_Parser/chronic/test/suite.rb new file mode 100644 index 000000000..fa8bdaab5 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/suite.rb @@ -0,0 +1,9 @@ +require 'test/unit' + +tests = Dir["#{File.dirname(__FILE__)}/test_*.rb"] +tests.delete_if { |o| o =~ /test_parsing/ } +tests.each do |file| + require file +end + +require File.dirname(__FILE__) + '/test_parsing.rb' \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_Chronic.rb b/framework/Horde_Date_Parser/chronic/test/test_Chronic.rb new file mode 100644 index 000000000..04fedb520 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_Chronic.rb @@ -0,0 +1,50 @@ +require 'chronic' +require 'test/unit' + +class TestChronic < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 UTC 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_post_normalize_am_pm_aliases + # affect wanted patterns + + tokens = [Chronic::Token.new("5:00"), Chronic::Token.new("morning")] + tokens[0].tag(Chronic::RepeaterTime.new("5:00")) + tokens[1].tag(Chronic::RepeaterDayPortion.new(:morning)) + + assert_equal :morning, tokens[1].tags[0].type + + tokens = Chronic.dealias_and_disambiguate_times(tokens, {}) + + assert_equal :am, tokens[1].tags[0].type + assert_equal 2, tokens.size + + # don't affect unwanted patterns + + tokens = [Chronic::Token.new("friday"), Chronic::Token.new("morning")] + tokens[0].tag(Chronic::RepeaterDayName.new(:friday)) + tokens[1].tag(Chronic::RepeaterDayPortion.new(:morning)) + + assert_equal :morning, tokens[1].tags[0].type + + tokens = Chronic.dealias_and_disambiguate_times(tokens, {}) + + assert_equal :morning, tokens[1].tags[0].type + assert_equal 2, tokens.size + end + + def test_guess + span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0)) + assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span) + + span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0, 0, 1)) + assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span) + + span = Chronic::Span.new(Time.local(2006, 11), Time.local(2006, 12)) + assert_equal Time.local(2006, 11, 16), Chronic.guess(span) + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_Handler.rb b/framework/Horde_Date_Parser/chronic/test/test_Handler.rb new file mode 100644 index 000000000..4e36dfe25 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_Handler.rb @@ -0,0 +1,110 @@ +require 'chronic' +require 'test/unit' + +class TestHandler < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 UTC 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_handler_class_1 + handler = Chronic::Handler.new([:repeater], :handler) + + tokens = [Chronic::Token.new('friday')] + tokens[0].tag(Chronic::RepeaterDayName.new(:friday)) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('afternoon') + tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon)) + + assert !handler.match(tokens, Chronic.definitions) + end + + def test_handler_class_2 + handler = Chronic::Handler.new([:repeater, :repeater?], :handler) + + tokens = [Chronic::Token.new('friday')] + tokens[0].tag(Chronic::RepeaterDayName.new(:friday)) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('afternoon') + tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon)) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('afternoon') + tokens[2].tag(Chronic::RepeaterDayPortion.new(:afternoon)) + + assert !handler.match(tokens, Chronic.definitions) + end + + def test_handler_class_3 + handler = Chronic::Handler.new([:repeater, 'time?'], :handler) + + tokens = [Chronic::Token.new('friday')] + tokens[0].tag(Chronic::RepeaterDayName.new(:friday)) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('afternoon') + tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon)) + + assert !handler.match(tokens, Chronic.definitions) + end + + def test_handler_class_4 + handler = Chronic::Handler.new([:repeater_month_name, :scalar_day, 'time?'], :handler) + + tokens = [Chronic::Token.new('may')] + tokens[0].tag(Chronic::RepeaterMonthName.new(:may)) + + assert !handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('27') + tokens[1].tag(Chronic::ScalarDay.new(27)) + + assert handler.match(tokens, Chronic.definitions) + end + + def test_handler_class_5 + handler = Chronic::Handler.new([:repeater, 'time?'], :handler) + + tokens = [Chronic::Token.new('friday')] + tokens[0].tag(Chronic::RepeaterDayName.new(:friday)) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('5:00') + tokens[1].tag(Chronic::RepeaterTime.new('5:00')) + + assert handler.match(tokens, Chronic.definitions) + + tokens << Chronic::Token.new('pm') + tokens[2].tag(Chronic::RepeaterDayPortion.new(:pm)) + + assert handler.match(tokens, Chronic.definitions) + end + + def test_handler_class_6 + handler = Chronic::Handler.new([:scalar, :repeater, :pointer], :handler) + + tokens = [Chronic::Token.new('3'), + Chronic::Token.new('years'), + Chronic::Token.new('past')] + + tokens[0].tag(Chronic::Scalar.new(3)) + tokens[1].tag(Chronic::RepeaterYear.new(:year)) + tokens[2].tag(Chronic::Pointer.new(:past)) + + assert handler.match(tokens, Chronic.definitions) + end + + def test_constantize + handler = Chronic::Handler.new([], :handler) + assert_equal Chronic::RepeaterTime, handler.constantize(:repeater_time) + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_Numerizer.rb b/framework/Horde_Date_Parser/chronic/test/test_Numerizer.rb new file mode 100644 index 000000000..f70c51ad8 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_Numerizer.rb @@ -0,0 +1,48 @@ +require 'test/unit' +require 'chronic' + +class ParseNumbersTest < Test::Unit::TestCase + + def test_straight_parsing + strings = { 1 => 'one', + 5 => 'five', + 10 => 'ten', + 11 => 'eleven', + 12 => 'twelve', + 13 => 'thirteen', + 14 => 'fourteen', + 15 => 'fifteen', + 16 => 'sixteen', + 17 => 'seventeen', + 18 => 'eighteen', + 19 => 'nineteen', + 20 => 'twenty', + 27 => 'twenty seven', + 31 => 'thirty-one', + 59 => 'fifty nine', + 100 => 'a hundred', + 100 => 'one hundred', + 150 => 'one hundred and fifty', + # 150 => 'one fifty', + 200 => 'two-hundred', + 500 => '5 hundred', + 999 => 'nine hundred and ninety nine', + 1_000 => 'one thousand', + 1_200 => 'twelve hundred', + 1_200 => 'one thousand two hundred', + 17_000 => 'seventeen thousand', + 21_473 => 'twentyone-thousand-four-hundred-and-seventy-three', + 74_002 => 'seventy four thousand and two', + 99_999 => 'ninety nine thousand nine hundred ninety nine', + 100_000 => '100 thousand', + 250_000 => 'two hundred fifty thousand', + 1_000_000 => 'one million', + 1_250_007 => 'one million two hundred fifty thousand and seven', + 1_000_000_000 => 'one billion', + 1_000_000_001 => 'one billion and one' } + + strings.keys.sort.each do |key| + assert_equal key, Numerizer.numerize(strings[key]).to_i + end + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterDayName.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterDayName.rb new file mode 100644 index 000000000..8e119db30 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_RepeaterDayName.rb @@ -0,0 +1,52 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterDayName < Test::Unit::TestCase + + def setup + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_match + token = Chronic::Token.new('saturday') + repeater = Chronic::Repeater.scan_for_day_names(token) + assert_equal Chronic::RepeaterDayName, repeater.class + assert_equal :saturday, repeater.type + + token = Chronic::Token.new('sunday') + repeater = Chronic::Repeater.scan_for_day_names(token) + assert_equal Chronic::RepeaterDayName, repeater.class + assert_equal :sunday, repeater.type + end + + def test_next_future + mondays = Chronic::RepeaterDayName.new(:monday) + mondays.start = @now + + span = mondays.next(:future) + + assert_equal Time.local(2006, 8, 21), span.begin + assert_equal Time.local(2006, 8, 22), span.end + + span = mondays.next(:future) + + assert_equal Time.local(2006, 8, 28), span.begin + assert_equal Time.local(2006, 8, 29), span.end + end + + def test_next_past + mondays = Chronic::RepeaterDayName.new(:monday) + mondays.start = @now + + span = mondays.next(:past) + + assert_equal Time.local(2006, 8, 14), span.begin + assert_equal Time.local(2006, 8, 15), span.end + + span = mondays.next(:past) + + assert_equal Time.local(2006, 8, 7), span.begin + assert_equal Time.local(2006, 8, 8), span.end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterFortnight.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterFortnight.rb new file mode 100644 index 000000000..cb80c43cf --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_RepeaterFortnight.rb @@ -0,0 +1,63 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterFortnight < Test::Unit::TestCase + + def setup + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + fortnights = Chronic::RepeaterFortnight.new(:fortnight) + fortnights.start = @now + + next_fortnight = fortnights.next(:future) + assert_equal Time.local(2006, 8, 20), next_fortnight.begin + assert_equal Time.local(2006, 9, 3), next_fortnight.end + + next_next_fortnight = fortnights.next(:future) + assert_equal Time.local(2006, 9, 3), next_next_fortnight.begin + assert_equal Time.local(2006, 9, 17), next_next_fortnight.end + end + + def test_next_past + fortnights = Chronic::RepeaterFortnight.new(:fortnight) + fortnights.start = @now + + last_fortnight = fortnights.next(:past) + assert_equal Time.local(2006, 7, 30), last_fortnight.begin + assert_equal Time.local(2006, 8, 13), last_fortnight.end + + last_last_fortnight = fortnights.next(:past) + assert_equal Time.local(2006, 7, 16), last_last_fortnight.begin + assert_equal Time.local(2006, 7, 30), last_last_fortnight.end + end + + def test_this_future + fortnights = Chronic::RepeaterFortnight.new(:fortnight) + fortnights.start = @now + + this_fortnight = fortnights.this(:future) + assert_equal Time.local(2006, 8, 16, 15), this_fortnight.begin + assert_equal Time.local(2006, 8, 27), this_fortnight.end + end + + def test_this_past + fortnights = Chronic::RepeaterFortnight.new(:fortnight) + fortnights.start = @now + + this_fortnight = fortnights.this(:past) + assert_equal Time.local(2006, 8, 13, 0), this_fortnight.begin + assert_equal Time.local(2006, 8, 16, 14), this_fortnight.end + end + + def test_offset + span = Chronic::Span.new(@now, @now + 1) + + offset_span = Chronic::RepeaterWeek.new(:week).offset(span, 3, :future) + + assert_equal Time.local(2006, 9, 6, 14), offset_span.begin + assert_equal Time.local(2006, 9, 6, 14, 0, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterHour.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterHour.rb new file mode 100644 index 000000000..48f37c429 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_RepeaterHour.rb @@ -0,0 +1,65 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterHour < Test::Unit::TestCase + + def setup + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + hours = Chronic::RepeaterHour.new(:hour) + hours.start = @now + + next_hour = hours.next(:future) + assert_equal Time.local(2006, 8, 16, 15), next_hour.begin + assert_equal Time.local(2006, 8, 16, 16), next_hour.end + + next_next_hour = hours.next(:future) + assert_equal Time.local(2006, 8, 16, 16), next_next_hour.begin + assert_equal Time.local(2006, 8, 16, 17), next_next_hour.end + end + + def test_next_past + hours = Chronic::RepeaterHour.new(:hour) + hours.start = @now + + past_hour = hours.next(:past) + assert_equal Time.local(2006, 8, 16, 13), past_hour.begin + assert_equal Time.local(2006, 8, 16, 14), past_hour.end + + past_past_hour = hours.next(:past) + assert_equal Time.local(2006, 8, 16, 12), past_past_hour.begin + assert_equal Time.local(2006, 8, 16, 13), past_past_hour.end + end + + def test_this + @now = Time.local(2006, 8, 16, 14, 30) + + hours = Chronic::RepeaterHour.new(:hour) + hours.start = @now + + this_hour = hours.this(:future) + assert_equal Time.local(2006, 8, 16, 14, 31), this_hour.begin + assert_equal Time.local(2006, 8, 16, 15), this_hour.end + + this_hour = hours.this(:past) + assert_equal Time.local(2006, 8, 16, 14), this_hour.begin + assert_equal Time.local(2006, 8, 16, 14, 30), this_hour.end + end + + def test_offset + span = Chronic::Span.new(@now, @now + 1) + + offset_span = Chronic::RepeaterHour.new(:hour).offset(span, 3, :future) + + assert_equal Time.local(2006, 8, 16, 17), offset_span.begin + assert_equal Time.local(2006, 8, 16, 17, 0, 1), offset_span.end + + offset_span = Chronic::RepeaterHour.new(:hour).offset(span, 24, :past) + + assert_equal Time.local(2006, 8, 15, 14), offset_span.begin + assert_equal Time.local(2006, 8, 15, 14, 0, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterMonth.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterMonth.rb new file mode 100644 index 000000000..d0609c5f0 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_RepeaterMonth.rb @@ -0,0 +1,47 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterMonth < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_offset_by + # future + + time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 1, :future) + assert_equal Time.local(2006, 9, 16, 14), time + + time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 5, :future) + assert_equal Time.local(2007, 1, 16, 14), time + + # past + + time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 1, :past) + assert_equal Time.local(2006, 7, 16, 14), time + + time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 10, :past) + assert_equal Time.local(2005, 10, 16, 14), time + end + + def test_offset + # future + + span = Chronic::Span.new(@now, @now + 60) + offset_span = Chronic::RepeaterMonth.new(:month).offset(span, 1, :future) + + assert_equal Time.local(2006, 9, 16, 14), offset_span.begin + assert_equal Time.local(2006, 9, 16, 14, 1), offset_span.end + + # past + + span = Chronic::Span.new(@now, @now + 60) + offset_span = Chronic::RepeaterMonth.new(:month).offset(span, 1, :past) + + assert_equal Time.local(2006, 7, 16, 14), offset_span.begin + assert_equal Time.local(2006, 7, 16, 14, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterMonthName.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterMonthName.rb new file mode 100644 index 000000000..6326a4587 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_RepeaterMonthName.rb @@ -0,0 +1,57 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterMonthName < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next + # future + + mays = Chronic::RepeaterMonthName.new(:may) + mays.start = @now + + next_may = mays.next(:future) + assert_equal Time.local(2007, 5), next_may.begin + assert_equal Time.local(2007, 6), next_may.end + + next_next_may = mays.next(:future) + assert_equal Time.local(2008, 5), next_next_may.begin + assert_equal Time.local(2008, 6), next_next_may.end + + decembers = Chronic::RepeaterMonthName.new(:december) + decembers.start = @now + + next_december = decembers.next(:future) + assert_equal Time.local(2006, 12), next_december.begin + assert_equal Time.local(2007, 1), next_december.end + + # past + + mays = Chronic::RepeaterMonthName.new(:may) + mays.start = @now + + assert_equal Time.local(2006, 5), mays.next(:past).begin + assert_equal Time.local(2005, 5), mays.next(:past).begin + end + + def test_this + octobers = Chronic::RepeaterMonthName.new(:october) + octobers.start = @now + + this_october = octobers.this(:future) + assert_equal Time.local(2006, 10, 1), this_october.begin + assert_equal Time.local(2006, 11, 1), this_october.end + + aprils = Chronic::RepeaterMonthName.new(:april) + aprils.start = @now + + this_april = aprils.this(:past) + assert_equal Time.local(2006, 4, 1), this_april.begin + assert_equal Time.local(2006, 5, 1), this_april.end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterTime.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterTime.rb new file mode 100644 index 000000000..bb2773588 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_RepeaterTime.rb @@ -0,0 +1,72 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterTime < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + t = Chronic::RepeaterTime.new('4:00') + t.start = @now + + assert_equal Time.local(2006, 8, 16, 16), t.next(:future).begin + assert_equal Time.local(2006, 8, 17, 4), t.next(:future).begin + + t = Chronic::RepeaterTime.new('13:00') + t.start = @now + + assert_equal Time.local(2006, 8, 17, 13), t.next(:future).begin + assert_equal Time.local(2006, 8, 18, 13), t.next(:future).begin + + t = Chronic::RepeaterTime.new('0400') + t.start = @now + + assert_equal Time.local(2006, 8, 17, 4), t.next(:future).begin + assert_equal Time.local(2006, 8, 18, 4), t.next(:future).begin + end + + def test_next_past + t = Chronic::RepeaterTime.new('4:00') + t.start = @now + + assert_equal Time.local(2006, 8, 16, 4), t.next(:past).begin + assert_equal Time.local(2006, 8, 15, 16), t.next(:past).begin + + t = Chronic::RepeaterTime.new('13:00') + t.start = @now + + assert_equal Time.local(2006, 8, 16, 13), t.next(:past).begin + assert_equal Time.local(2006, 8, 15, 13), t.next(:past).begin + end + + def test_type + t1 = Chronic::RepeaterTime.new('4') + assert_equal 14_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('14') + assert_equal 50_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('4:00') + assert_equal 14_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('4:30') + assert_equal 16_200, t1.type.time + + t1 = Chronic::RepeaterTime.new('1400') + assert_equal 50_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('0400') + assert_equal 14_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('04') + assert_equal 14_400, t1.type.time + + t1 = Chronic::RepeaterTime.new('400') + assert_equal 14_400, t1.type.time + end + + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterWeek.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterWeek.rb new file mode 100644 index 000000000..084ef4ee6 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_RepeaterWeek.rb @@ -0,0 +1,63 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterWeek < Test::Unit::TestCase + + def setup + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + weeks = Chronic::RepeaterWeek.new(:week) + weeks.start = @now + + next_week = weeks.next(:future) + assert_equal Time.local(2006, 8, 20), next_week.begin + assert_equal Time.local(2006, 8, 27), next_week.end + + next_next_week = weeks.next(:future) + assert_equal Time.local(2006, 8, 27), next_next_week.begin + assert_equal Time.local(2006, 9, 3), next_next_week.end + end + + def test_next_past + weeks = Chronic::RepeaterWeek.new(:week) + weeks.start = @now + + last_week = weeks.next(:past) + assert_equal Time.local(2006, 8, 6), last_week.begin + assert_equal Time.local(2006, 8, 13), last_week.end + + last_last_week = weeks.next(:past) + assert_equal Time.local(2006, 7, 30), last_last_week.begin + assert_equal Time.local(2006, 8, 6), last_last_week.end + end + + def test_this_future + weeks = Chronic::RepeaterWeek.new(:week) + weeks.start = @now + + this_week = weeks.this(:future) + assert_equal Time.local(2006, 8, 16, 15), this_week.begin + assert_equal Time.local(2006, 8, 20), this_week.end + end + + def test_this_past + weeks = Chronic::RepeaterWeek.new(:week) + weeks.start = @now + + this_week = weeks.this(:past) + assert_equal Time.local(2006, 8, 13, 0), this_week.begin + assert_equal Time.local(2006, 8, 16, 14), this_week.end + end + + def test_offset + span = Chronic::Span.new(@now, @now + 1) + + offset_span = Chronic::RepeaterWeek.new(:week).offset(span, 3, :future) + + assert_equal Time.local(2006, 9, 6, 14), offset_span.begin + assert_equal Time.local(2006, 9, 6, 14, 0, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterWeekend.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterWeekend.rb new file mode 100644 index 000000000..44dc08763 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_RepeaterWeekend.rb @@ -0,0 +1,75 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterWeekend < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = @now + + next_weekend = weekend.next(:future) + assert_equal Time.local(2006, 8, 19), next_weekend.begin + assert_equal Time.local(2006, 8, 21), next_weekend.end + end + + def test_next_past + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = @now + + next_weekend = weekend.next(:past) + assert_equal Time.local(2006, 8, 12), next_weekend.begin + assert_equal Time.local(2006, 8, 14), next_weekend.end + end + + def test_this_future + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = @now + + next_weekend = weekend.this(:future) + assert_equal Time.local(2006, 8, 19), next_weekend.begin + assert_equal Time.local(2006, 8, 21), next_weekend.end + end + + def test_this_past + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = @now + + next_weekend = weekend.this(:past) + assert_equal Time.local(2006, 8, 12), next_weekend.begin + assert_equal Time.local(2006, 8, 14), next_weekend.end + end + + def test_this_none + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = @now + + next_weekend = weekend.this(:future) + assert_equal Time.local(2006, 8, 19), next_weekend.begin + assert_equal Time.local(2006, 8, 21), next_weekend.end + end + + def test_offset + span = Chronic::Span.new(@now, @now + 1) + + offset_span = Chronic::RepeaterWeekend.new(:weekend).offset(span, 3, :future) + + assert_equal Time.local(2006, 9, 2), offset_span.begin + assert_equal Time.local(2006, 9, 2, 0, 0, 1), offset_span.end + + offset_span = Chronic::RepeaterWeekend.new(:weekend).offset(span, 1, :past) + + assert_equal Time.local(2006, 8, 12), offset_span.begin + assert_equal Time.local(2006, 8, 12, 0, 0, 1), offset_span.end + + offset_span = Chronic::RepeaterWeekend.new(:weekend).offset(span, 0, :future) + + assert_equal Time.local(2006, 8, 12), offset_span.begin + assert_equal Time.local(2006, 8, 12, 0, 0, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterYear.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterYear.rb new file mode 100644 index 000000000..eaebe25a2 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_RepeaterYear.rb @@ -0,0 +1,63 @@ +require 'chronic' +require 'test/unit' + +class TestRepeaterYear < Test::Unit::TestCase + + def setup + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_next_future + years = Chronic::RepeaterYear.new(:year) + years.start = @now + + next_year = years.next(:future) + assert_equal Time.local(2007, 1, 1), next_year.begin + assert_equal Time.local(2008, 1, 1), next_year.end + + next_next_year = years.next(:future) + assert_equal Time.local(2008, 1, 1), next_next_year.begin + assert_equal Time.local(2009, 1, 1), next_next_year.end + end + + def test_next_past + years = Chronic::RepeaterYear.new(:year) + years.start = @now + + last_year = years.next(:past) + assert_equal Time.local(2005, 1, 1), last_year.begin + assert_equal Time.local(2006, 1, 1), last_year.end + + last_last_year = years.next(:past) + assert_equal Time.local(2004, 1, 1), last_last_year.begin + assert_equal Time.local(2005, 1, 1), last_last_year.end + end + + def test_this + years = Chronic::RepeaterYear.new(:year) + years.start = @now + + this_year = years.this(:future) + assert_equal Time.local(2006, 8, 17), this_year.begin + assert_equal Time.local(2007, 1, 1), this_year.end + + this_year = years.this(:past) + assert_equal Time.local(2006, 1, 1), this_year.begin + assert_equal Time.local(2006, 8, 16), this_year.end + end + + def test_offset + span = Chronic::Span.new(@now, @now + 1) + + offset_span = Chronic::RepeaterYear.new(:year).offset(span, 3, :future) + + assert_equal Time.local(2009, 8, 16, 14), offset_span.begin + assert_equal Time.local(2009, 8, 16, 14, 0, 1), offset_span.end + + offset_span = Chronic::RepeaterYear.new(:year).offset(span, 10, :past) + + assert_equal Time.local(1996, 8, 16, 14), offset_span.begin + assert_equal Time.local(1996, 8, 16, 14, 0, 1), offset_span.end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_Span.rb b/framework/Horde_Date_Parser/chronic/test/test_Span.rb new file mode 100644 index 000000000..099455a2d --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_Span.rb @@ -0,0 +1,24 @@ +require 'chronic' +require 'test/unit' + +class TestSpan < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 UTC 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_span_width + span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0)) + assert_equal (60 * 60 * 24), span.width + end + + def test_span_math + s = Chronic::Span.new(1, 2) + assert_equal 2, (s + 1).begin + assert_equal 3, (s + 1).end + assert_equal 0, (s - 1).begin + assert_equal 1, (s - 1).end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_Time.rb b/framework/Horde_Date_Parser/chronic/test/test_Time.rb new file mode 100644 index 000000000..3ffc9c019 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_Time.rb @@ -0,0 +1,50 @@ +require 'chronic' +require 'test/unit' + +class TestTime < Test::Unit::TestCase + + def setup + end + + def test_normal + assert_equal Time.local(2006, 1, 2, 0, 0, 0), Time.construct(2006, 1, 2, 0, 0, 0) + assert_equal Time.local(2006, 1, 2, 3, 0, 0), Time.construct(2006, 1, 2, 3, 0, 0) + assert_equal Time.local(2006, 1, 2, 3, 4, 0), Time.construct(2006, 1, 2, 3, 4, 0) + assert_equal Time.local(2006, 1, 2, 3, 4, 5), Time.construct(2006, 1, 2, 3, 4, 5) + end + + def test_second_overflow + assert_equal Time.local(2006, 1, 1, 0, 1, 30), Time.construct(2006, 1, 1, 0, 0, 90) + assert_equal Time.local(2006, 1, 1, 0, 5, 0), Time.construct(2006, 1, 1, 0, 0, 300) + end + + def test_minute_overflow + assert_equal Time.local(2006, 1, 1, 1, 30), Time.construct(2006, 1, 1, 0, 90) + assert_equal Time.local(2006, 1, 1, 5), Time.construct(2006, 1, 1, 0, 300) + end + + def test_hour_overflow + assert_equal Time.local(2006, 1, 2, 12), Time.construct(2006, 1, 1, 36) + assert_equal Time.local(2006, 1, 7), Time.construct(2006, 1, 1, 144) + end + + def test_day_overflow + assert_equal Time.local(2006, 2, 1), Time.construct(2006, 1, 32) + assert_equal Time.local(2006, 3, 5), Time.construct(2006, 2, 33) + assert_equal Time.local(2004, 3, 4), Time.construct(2004, 2, 33) + assert_equal Time.local(2000, 3, 5), Time.construct(2000, 2, 33) + + assert_nothing_raised do + Time.construct(2006, 1, 56) + end + + assert_raise(RuntimeError) do + Time.construct(2006, 1, 57) + end + end + + def test_month_overflow + assert_equal Time.local(2006, 1), Time.construct(2005, 13) + assert_equal Time.local(2005, 12), Time.construct(2000, 72) + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_Token.rb b/framework/Horde_Date_Parser/chronic/test/test_Token.rb new file mode 100644 index 000000000..80463d1ee --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_Token.rb @@ -0,0 +1,26 @@ +require 'chronic' +require 'test/unit' + +class TestToken < Test::Unit::TestCase + + def setup + # Wed Aug 16 14:00:00 UTC 2006 + @now = Time.local(2006, 8, 16, 14, 0, 0, 0) + end + + def test_token + token = Chronic::Token.new('foo') + assert_equal 0, token.tags.size + assert !token.tagged? + token.tag("mytag") + assert_equal 1, token.tags.size + assert token.tagged? + assert_equal String, token.get_tag(String).class + token.tag(5) + assert_equal 2, token.tags.size + token.untag(String) + assert_equal 1, token.tags.size + assert_equal 'foo', token.word + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/chronic/test/test_parsing.rb b/framework/Horde_Date_Parser/chronic/test/test_parsing.rb new file mode 100644 index 000000000..d2216d676 --- /dev/null +++ b/framework/Horde_Date_Parser/chronic/test/test_parsing.rb @@ -0,0 +1,614 @@ +require 'chronic' +require 'time' +require 'test/unit' + +class TestParsing < Test::Unit::TestCase + # Wed Aug 16 14:00:00 UTC 2006 + TIME_2006_08_16_14_00_00 = Time.local(2006, 8, 16, 14, 0, 0, 0) + + def setup + @time_2006_08_16_14_00_00 = TIME_2006_08_16_14_00_00 + end + + def test_parse_guess_dates + # rm_sd + + time = parse_now("may 27") + assert_equal Time.local(2007, 5, 27, 12), time + + time = parse_now("may 28", :context => :past) + assert_equal Time.local(2006, 5, 28, 12), time + + time = parse_now("may 28 5pm", :context => :past) + assert_equal Time.local(2006, 5, 28, 17), time + + time = parse_now("may 28 at 5pm", :context => :past) + assert_equal Time.local(2006, 5, 28, 17), time + + time = parse_now("may 28 at 5:32.19pm", :context => :past) + assert_equal Time.local(2006, 5, 28, 17, 32, 19), time + + # rm_od + + time = parse_now("may 27th") + assert_equal Time.local(2007, 5, 27, 12), time + + time = parse_now("may 27th", :context => :past) + assert_equal Time.local(2006, 5, 27, 12), time + + time = parse_now("may 27th 5:00 pm", :context => :past) + assert_equal Time.local(2006, 5, 27, 17), time + + time = parse_now("may 27th at 5pm", :context => :past) + assert_equal Time.local(2006, 5, 27, 17), time + + time = parse_now("may 27th at 5", :ambiguous_time_range => :none) + assert_equal Time.local(2007, 5, 27, 5), time + + # rm_sy + + time = parse_now("June 1979") + assert_equal Time.local(1979, 6, 16, 0), time + + time = parse_now("dec 79") + assert_equal Time.local(1979, 12, 16, 12), time + + # rm_sd_sy + + time = parse_now("jan 3 2010") + assert_equal Time.local(2010, 1, 3, 12), time + + time = parse_now("jan 3 2010 midnight") + assert_equal Time.local(2010, 1, 4, 0), time + + time = parse_now("jan 3 2010 at midnight") + assert_equal Time.local(2010, 1, 4, 0), time + + time = parse_now("jan 3 2010 at 4", :ambiguous_time_range => :none) + assert_equal Time.local(2010, 1, 3, 4), time + + #time = parse_now("January 12, '00") + #assert_equal Time.local(2000, 1, 12, 12), time + + time = parse_now("may 27 79") + assert_equal Time.local(1979, 5, 27, 12), time + + time = parse_now("may 27 79 4:30") + assert_equal Time.local(1979, 5, 27, 16, 30), time + + time = parse_now("may 27 79 at 4:30", :ambiguous_time_range => :none) + assert_equal Time.local(1979, 5, 27, 4, 30), time + + # sd_rm_sy + + time = parse_now("3 jan 2010") + assert_equal Time.local(2010, 1, 3, 12), time + + time = parse_now("3 jan 2010 4pm") + assert_equal Time.local(2010, 1, 3, 16), time + + # sm_sd_sy + + time = parse_now("5/27/1979") + assert_equal Time.local(1979, 5, 27, 12), time + + time = parse_now("5/27/1979 4am") + assert_equal Time.local(1979, 5, 27, 4), time + + # sd_sm_sy + + time = parse_now("27/5/1979") + assert_equal Time.local(1979, 5, 27, 12), time + + time = parse_now("27/5/1979 @ 0700") + assert_equal Time.local(1979, 5, 27, 7), time + + # sm_sy + + time = parse_now("05/06") + assert_equal Time.local(2006, 5, 16, 12), time + + time = parse_now("12/06") + assert_equal Time.local(2006, 12, 16, 12), time + + time = parse_now("13/06") + assert_equal nil, time + + # sy_sm_sd + + time = parse_now("2000-1-1") + assert_equal Time.local(2000, 1, 1, 12), time + + time = parse_now("2006-08-20") + assert_equal Time.local(2006, 8, 20, 12), time + + time = parse_now("2006-08-20 7pm") + assert_equal Time.local(2006, 8, 20, 19), time + + time = parse_now("2006-08-20 03:00") + assert_equal Time.local(2006, 8, 20, 3), time + + time = parse_now("2006-08-20 03:30:30") + assert_equal Time.local(2006, 8, 20, 3, 30, 30), time + + time = parse_now("2006-08-20 15:30:30") + assert_equal Time.local(2006, 8, 20, 15, 30, 30), time + + time = parse_now("2006-08-20 15:30.30") + assert_equal Time.local(2006, 8, 20, 15, 30, 30), time + + # rdn_rm_rd_rt_rtz_ry + + time = parse_now("Mon Apr 02 17:00:00 PDT 2007") + assert_equal Time.local(2007, 4, 2, 17), time + + now = Time.now + time = parse_now(now.to_s) + assert_equal now.to_s, time.to_s + + # rm_sd_rt + + #time = parse_now("jan 5 13:00") + #assert_equal Time.local(2007, 1, 5, 13), time + + # due to limitations of the Time class, these don't work + + time = parse_now("may 40") + assert_equal nil, time + + time = parse_now("may 27 40") + assert_equal nil, time + + time = parse_now("1800-08-20") + assert_equal nil, time + end + + def test_foo + Chronic.parse('two months ago this friday') + end + + def test_parse_guess_r + time = parse_now("friday") + assert_equal Time.local(2006, 8, 18, 12), time + + time = parse_now("tue") + assert_equal Time.local(2006, 8, 22, 12), time + + time = parse_now("5") + assert_equal Time.local(2006, 8, 16, 17), time + + time = Chronic.parse("5", :now => Time.local(2006, 8, 16, 3, 0, 0, 0), :ambiguous_time_range => :none) + assert_equal Time.local(2006, 8, 16, 5), time + + time = parse_now("13:00") + assert_equal Time.local(2006, 8, 17, 13), time + + time = parse_now("13:45") + assert_equal Time.local(2006, 8, 17, 13, 45), time + + time = parse_now("november") + assert_equal Time.local(2006, 11, 16), time + end + + def test_parse_guess_rr + time = parse_now("friday 13:00") + assert_equal Time.local(2006, 8, 18, 13), time + + time = parse_now("monday 4:00") + assert_equal Time.local(2006, 8, 21, 16), time + + time = parse_now("sat 4:00", :ambiguous_time_range => :none) + assert_equal Time.local(2006, 8, 19, 4), time + + time = parse_now("sunday 4:20", :ambiguous_time_range => :none) + assert_equal Time.local(2006, 8, 20, 4, 20), time + + time = parse_now("4 pm") + assert_equal Time.local(2006, 8, 16, 16), time + + time = parse_now("4 am", :ambiguous_time_range => :none) + assert_equal Time.local(2006, 8, 16, 4), time + + time = parse_now("12 pm") + assert_equal Time.local(2006, 8, 16, 12), time + + time = parse_now("12:01 pm") + assert_equal Time.local(2006, 8, 16, 12, 1), time + + time = parse_now("12:01 am") + assert_equal Time.local(2006, 8, 16, 0, 1), time + + time = parse_now("12 am") + assert_equal Time.local(2006, 8, 16), time + + time = parse_now("4:00 in the morning") + assert_equal Time.local(2006, 8, 16, 4), time + + time = parse_now("november 4") + assert_equal Time.local(2006, 11, 4, 12), time + + time = parse_now("aug 24") + assert_equal Time.local(2006, 8, 24, 12), time + end + + def test_parse_guess_rrr + time = parse_now("friday 1 pm") + assert_equal Time.local(2006, 8, 18, 13), time + + time = parse_now("friday 11 at night") + assert_equal Time.local(2006, 8, 18, 23), time + + time = parse_now("friday 11 in the evening") + assert_equal Time.local(2006, 8, 18, 23), time + + time = parse_now("sunday 6am") + assert_equal Time.local(2006, 8, 20, 6), time + + time = parse_now("friday evening at 7") + assert_equal Time.local(2006, 8, 18, 19), time + end + + def test_parse_guess_gr + # year + + time = parse_now("this year") + assert_equal Time.local(2006, 10, 24, 12, 30), time + + time = parse_now("this year", :context => :past) + assert_equal Time.local(2006, 4, 24, 12, 30), time + + # month + + time = parse_now("this month") + assert_equal Time.local(2006, 8, 24, 12), time + + time = parse_now("this month", :context => :past) + assert_equal Time.local(2006, 8, 8, 12), time + + time = Chronic.parse("next month", :now => Time.local(2006, 11, 15)) + assert_equal Time.local(2006, 12, 16, 12), time + + # month name + + time = parse_now("last november") + assert_equal Time.local(2005, 11, 16), time + + # fortnight + + time = parse_now("this fortnight") + assert_equal Time.local(2006, 8, 21, 19, 30), time + + time = parse_now("this fortnight", :context => :past) + assert_equal Time.local(2006, 8, 14, 19), time + + # week + + time = parse_now("this week") + assert_equal Time.local(2006, 8, 18, 7, 30), time + + time = parse_now("this week", :context => :past) + assert_equal Time.local(2006, 8, 14, 19), time + + # weekend + + time = parse_now("this weekend") + assert_equal Time.local(2006, 8, 20), time + + time = parse_now("this weekend", :context => :past) + assert_equal Time.local(2006, 8, 13), time + + time = parse_now("last weekend") + assert_equal Time.local(2006, 8, 13), time + + # day + + time = parse_now("this day") + assert_equal Time.local(2006, 8, 16, 19, 30), time + + time = parse_now("this day", :context => :past) + assert_equal Time.local(2006, 8, 16, 7), time + + time = parse_now("today") + assert_equal Time.local(2006, 8, 16, 19, 30), time + + time = parse_now("yesterday") + assert_equal Time.local(2006, 8, 15, 12), time + + time = parse_now("tomorrow") + assert_equal Time.local(2006, 8, 17, 12), time + + # day name + + time = parse_now("this tuesday") + assert_equal Time.local(2006, 8, 22, 12), time + + time = parse_now("next tuesday") + assert_equal Time.local(2006, 8, 22, 12), time + + time = parse_now("last tuesday") + assert_equal Time.local(2006, 8, 15, 12), time + + time = parse_now("this wed") + assert_equal Time.local(2006, 8, 23, 12), time + + time = parse_now("next wed") + assert_equal Time.local(2006, 8, 23, 12), time + + time = parse_now("last wed") + assert_equal Time.local(2006, 8, 9, 12), time + + # day portion + + time = parse_now("this morning") + assert_equal Time.local(2006, 8, 16, 9), time + + time = parse_now("tonight") + assert_equal Time.local(2006, 8, 16, 22), time + + # minute + + time = parse_now("next minute") + assert_equal Time.local(2006, 8, 16, 14, 1, 30), time + + # second + + time = parse_now("this second") + assert_equal Time.local(2006, 8, 16, 14), time + + time = parse_now("this second", :context => :past) + assert_equal Time.local(2006, 8, 16, 14), time + + time = parse_now("next second") + assert_equal Time.local(2006, 8, 16, 14, 0, 1), time + + time = parse_now("last second") + assert_equal Time.local(2006, 8, 16, 13, 59, 59), time + end + + def test_parse_guess_grr + time = parse_now("yesterday at 4:00") + assert_equal Time.local(2006, 8, 15, 16), time + + time = parse_now("today at 9:00") + assert_equal Time.local(2006, 8, 16, 9), time + + time = parse_now("today at 2100") + assert_equal Time.local(2006, 8, 16, 21), time + + time = parse_now("this day at 0900") + assert_equal Time.local(2006, 8, 16, 9), time + + time = parse_now("tomorrow at 0900") + assert_equal Time.local(2006, 8, 17, 9), time + + time = parse_now("yesterday at 4:00", :ambiguous_time_range => :none) + assert_equal Time.local(2006, 8, 15, 4), time + + time = parse_now("last friday at 4:00") + assert_equal Time.local(2006, 8, 11, 16), time + + time = parse_now("next wed 4:00") + assert_equal Time.local(2006, 8, 23, 16), time + + time = parse_now("yesterday afternoon") + assert_equal Time.local(2006, 8, 15, 15), time + + time = parse_now("last week tuesday") + assert_equal Time.local(2006, 8, 8, 12), time + + time = parse_now("tonight at 7") + assert_equal Time.local(2006, 8, 16, 19), time + + time = parse_now("tonight 7") + assert_equal Time.local(2006, 8, 16, 19), time + + time = parse_now("7 tonight") + assert_equal Time.local(2006, 8, 16, 19), time + end + + def test_parse_guess_grrr + time = parse_now("today at 6:00pm") + assert_equal Time.local(2006, 8, 16, 18), time + + time = parse_now("today at 6:00am") + assert_equal Time.local(2006, 8, 16, 6), time + + time = parse_now("this day 1800") + assert_equal Time.local(2006, 8, 16, 18), time + + time = parse_now("yesterday at 4:00pm") + assert_equal Time.local(2006, 8, 15, 16), time + + time = parse_now("tomorrow evening at 7") + assert_equal Time.local(2006, 8, 17, 19), time + + time = parse_now("tomorrow morning at 5:30") + assert_equal Time.local(2006, 8, 17, 5, 30), time + + time = parse_now("next monday at 12:01 am") + assert_equal Time.local(2006, 8, 21, 00, 1), time + + time = parse_now("next monday at 12:01 pm") + assert_equal Time.local(2006, 8, 21, 12, 1), time + end + + def test_parse_guess_rgr + time = parse_now("afternoon yesterday") + assert_equal Time.local(2006, 8, 15, 15), time + + time = parse_now("tuesday last week") + assert_equal Time.local(2006, 8, 8, 12), time + end + + def test_parse_guess_s_r_p + # past + + time = parse_now("3 years ago") + assert_equal Time.local(2003, 8, 16, 14), time + + time = parse_now("1 month ago") + assert_equal Time.local(2006, 7, 16, 14), time + + time = parse_now("1 fortnight ago") + assert_equal Time.local(2006, 8, 2, 14), time + + time = parse_now("2 fortnights ago") + assert_equal Time.local(2006, 7, 19, 14), time + + time = parse_now("3 weeks ago") + assert_equal Time.local(2006, 7, 26, 14), time + + time = parse_now("2 weekends ago") + assert_equal Time.local(2006, 8, 5), time + + time = parse_now("3 days ago") + assert_equal Time.local(2006, 8, 13, 14), time + + #time = parse_now("1 monday ago") + #assert_equal Time.local(2006, 8, 14, 12), time + + time = parse_now("5 mornings ago") + assert_equal Time.local(2006, 8, 12, 9), time + + time = parse_now("7 hours ago") + assert_equal Time.local(2006, 8, 16, 7), time + + time = parse_now("3 minutes ago") + assert_equal Time.local(2006, 8, 16, 13, 57), time + + time = parse_now("20 seconds before now") + assert_equal Time.local(2006, 8, 16, 13, 59, 40), time + + # future + + time = parse_now("3 years from now") + assert_equal Time.local(2009, 8, 16, 14, 0, 0), time + + time = parse_now("6 months hence") + assert_equal Time.local(2007, 2, 16, 14), time + + time = parse_now("3 fortnights hence") + assert_equal Time.local(2006, 9, 27, 14), time + + time = parse_now("1 week from now") + assert_equal Time.local(2006, 8, 23, 14, 0, 0), time + + time = parse_now("1 weekend from now") + assert_equal Time.local(2006, 8, 19), time + + time = parse_now("2 weekends from now") + assert_equal Time.local(2006, 8, 26), time + + time = parse_now("1 day hence") + assert_equal Time.local(2006, 8, 17, 14), time + + time = parse_now("5 mornings hence") + assert_equal Time.local(2006, 8, 21, 9), time + + time = parse_now("1 hour from now") + assert_equal Time.local(2006, 8, 16, 15), time + + time = parse_now("20 minutes hence") + assert_equal Time.local(2006, 8, 16, 14, 20), time + + time = parse_now("20 seconds from now") + assert_equal Time.local(2006, 8, 16, 14, 0, 20), time + + time = Chronic.parse("2 months ago", :now => Time.parse("2007-03-07 23:30")) + assert_equal Time.local(2007, 1, 7, 23, 30), time + end + + def test_parse_guess_p_s_r + time = parse_now("in 3 hours") + assert_equal Time.local(2006, 8, 16, 17), time + end + + def test_parse_guess_s_r_p_a + # past + + time = parse_now("3 years ago tomorrow") + assert_equal Time.local(2003, 8, 17, 12), time + + time = parse_now("3 years ago this friday") + assert_equal Time.local(2003, 8, 18, 12), time + + time = parse_now("3 months ago saturday at 5:00 pm") + assert_equal Time.local(2006, 5, 19, 17), time + + time = parse_now("2 days from this second") + assert_equal Time.local(2006, 8, 18, 14), time + + time = parse_now("7 hours before tomorrow at midnight") + assert_equal Time.local(2006, 8, 17, 17), time + + # future + end + + def test_parse_guess_o_r_s_r + time = parse_now("3rd wednesday in november") + assert_equal Time.local(2006, 11, 15, 12), time + + time = parse_now("10th wednesday in november") + assert_equal nil, time + + # time = parse_now("3rd wednesday in 2007") + # assert_equal Time.local(2007, 1, 20, 12), time + end + + def test_parse_guess_o_r_g_r + time = parse_now("3rd month next year") + assert_equal Time.local(2007, 3, 16, 12, 30), time + + time = parse_now("3rd thursday this september") + assert_equal Time.local(2006, 9, 21, 12), time + + time = parse_now("4th day last week") + assert_equal Time.local(2006, 8, 9, 12), time + end + + def test_parse_guess_nonsense + time = parse_now("some stupid nonsense") + assert_equal nil, time + end + + def test_parse_span + span = parse_now("friday", :guess => false) + assert_equal Time.local(2006, 8, 18), span.begin + assert_equal Time.local(2006, 8, 19), span.end + + span = parse_now("november", :guess => false) + assert_equal Time.local(2006, 11), span.begin + assert_equal Time.local(2006, 12), span.end + + span = Chronic.parse("weekend" , :now => @time_2006_08_16_14_00_00, :guess => false) + assert_equal Time.local(2006, 8, 19), span.begin + assert_equal Time.local(2006, 8, 21), span.end + end + + def test_parse_words + assert_equal parse_now("33 days from now"), parse_now("thirty-three days from now") + assert_equal parse_now("2867532 seconds from now"), parse_now("two million eight hundred and sixty seven thousand five hundred and thirty two seconds from now") + assert_equal parse_now("may 10th"), parse_now("may tenth") + end + + def test_parse_only_complete_pointers + assert_equal parse_now("eat pasty buns today at 2pm"), @time_2006_08_16_14_00_00 + assert_equal parse_now("futuristically speaking today at 2pm"), @time_2006_08_16_14_00_00 + assert_equal parse_now("meeting today at 2pm"), @time_2006_08_16_14_00_00 + end + + def test_argument_validation + assert_raise(Chronic::InvalidArgumentException) do + time = Chronic.parse("may 27", :foo => :bar) + end + + assert_raise(Chronic::InvalidArgumentException) do + time = Chronic.parse("may 27", :context => :bar) + end + end + + private + def parse_now(string, options={}) + Chronic.parse(string, {:now => TIME_2006_08_16_14_00_00 }.merge(options)) + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser.php new file mode 100644 index 000000000..477240d97 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser.php @@ -0,0 +1,53 @@ + +alias p_orig p + +def p(val) + p_orig val + puts +end + +class Time + def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) + if second >= 60 + minute += second / 60 + second = second % 60 + end + + if minute >= 60 + hour += minute / 60 + minute = minute % 60 + end + + if hour >= 24 + day += hour / 24 + hour = hour % 24 + end + + # determine if there is a day overflow. this is complicated by our crappy calendar + # system (non-constant number of days per month) + day <= 56 || raise("day must be no more than 56 (makes month resolution easier)") + if day > 28 + # no month ever has fewer than 28 days, so only do this if necessary + leap_year = (year % 4 == 0) && !(year % 100 == 0) + leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + days_this_month = leap_year ? leap_year_month_days[month - 1] : common_year_month_days[month - 1] + if day > days_this_month + month += day / days_this_month + day = day % days_this_month + end + end + + if month > 12 + if month % 12 == 0 + year += (month - 12) / 12 + month = 12 + else + year += month / 12 + month = month % 12 + end + end + + Time.local(year, month, day, hour, minute, second) + end +end diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Grabber.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Grabber.php new file mode 100644 index 000000000..4162a260b --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Grabber.php @@ -0,0 +1,26 @@ +#module Chronic + + class Chronic::Grabber < Chronic::Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/last/ => :last, + /this/ => :this, + /next/ => :next} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'grabber-' << @type.to_s + end + end + +#end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Handlers.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Handlers.php new file mode 100644 index 000000000..551d632fa --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Handlers.php @@ -0,0 +1,469 @@ +module Chronic + + class << self + + def definitions #:nodoc: + @definitions ||= + {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)], + + :date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy), + Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd), + Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od), + Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy), + Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy), + Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy), + Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy), + Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd), + Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)], + + # tonight at 7pm + :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r), + Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r), + Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)], + + # 3 weeks from now, in 2 months + :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p), + Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r), + Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)], + + # 3rd week in march + :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r), + Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)] + } + end + + def tokens_to_span(tokens, options) #:nodoc: + # maybe it's a specific date + + self.definitions[:date].each do |handler| + if handler.match(tokens, self.definitions) + puts "-date" if Chronic.debug + good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # I guess it's not a specific date, maybe it's just an anchor + + self.definitions[:anchor].each do |handler| + if handler.match(tokens, self.definitions) + puts "-anchor" if Chronic.debug + good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # not an anchor, perhaps it's an arrow + + self.definitions[:arrow].each do |handler| + if handler.match(tokens, self.definitions) + puts "-arrow" if Chronic.debug + good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) } + return self.send(handler.handler_method, good_tokens, options) + end + end + + # not an arrow, let's hope it's a narrow + + self.definitions[:narrow].each do |handler| + if handler.match(tokens, self.definitions) + puts "-narrow" if Chronic.debug + #good_tokens = tokens.select { |o| !o.get_tag Separator } + return self.send(handler.handler_method, tokens, options) + end + end + + # I guess you're out of luck! + puts "-none" if Chronic.debug + return nil + end + + #-------------- + + def day_or_time(day_start, time_tokens, options) + outer_span = Span.new(day_start, day_start + (24 * 60 * 60)) + + if !time_tokens.empty? + @now = outer_span.begin + time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options) + return time + else + return outer_span + end + end + + #-------------- + + def handle_m_d(month, day, time_tokens, options) #:nodoc: + month.start = @now + span = month.this(options[:context]) + + day_start = Time.local(span.begin.year, span.begin.month, day) + + day_or_time(day_start, time_tokens, options) + end + + def handle_rmn_sd(tokens, options) #:nodoc: + handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options) + end + + def handle_rmn_od(tokens, options) #:nodoc: + handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options) + end + + def handle_rmn_sy(tokens, options) #:nodoc: + month = tokens[0].get_tag(RepeaterMonthName).index + year = tokens[1].get_tag(ScalarYear).type + + if month == 12 + next_month_year = year + 1 + next_month_month = 1 + else + next_month_year = year + next_month_month = month + 1 + end + + begin + Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month)) + rescue ArgumentError + nil + end + end + + def handle_rdn_rmn_sd_t_tz_sy(tokens, options) #:nodoc: + month = tokens[1].get_tag(RepeaterMonthName).index + day = tokens[2].get_tag(ScalarDay).type + year = tokens[5].get_tag(ScalarYear).type + + begin + day_start = Time.local(year, month, day) + day_or_time(day_start, [tokens[3]], options) + rescue ArgumentError + nil + end + end + + def handle_rmn_sd_sy(tokens, options) #:nodoc: + month = tokens[0].get_tag(RepeaterMonthName).index + day = tokens[1].get_tag(ScalarDay).type + year = tokens[2].get_tag(ScalarYear).type + + time_tokens = tokens.last(tokens.size - 3) + + begin + day_start = Time.local(year, month, day) + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + def handle_sd_rmn_sy(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_rmn_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sm_sd_sy(tokens, options) #:nodoc: + month = tokens[0].get_tag(ScalarMonth).type + day = tokens[1].get_tag(ScalarDay).type + year = tokens[2].get_tag(ScalarYear).type + + time_tokens = tokens.last(tokens.size - 3) + + begin + day_start = Time.local(year, month, day) #:nodoc: + day_or_time(day_start, time_tokens, options) + rescue ArgumentError + nil + end + end + + def handle_sd_sm_sy(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_sm_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sy_sm_sd(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[2], tokens[0]] + time_tokens = tokens.last(tokens.size - 3) + self.handle_sm_sd_sy(new_tokens + time_tokens, options) + end + + def handle_sm_sy(tokens, options) #:nodoc: + month = tokens[0].get_tag(ScalarMonth).type + year = tokens[1].get_tag(ScalarYear).type + + if month == 12 + next_month_year = year + 1 + next_month_month = 1 + else + next_month_year = year + next_month_month = month + 1 + end + + begin + Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month)) + rescue ArgumentError + nil + end + end + + # anchors + + def handle_r(tokens, options) #:nodoc: + dd_tokens = dealias_and_disambiguate_times(tokens, options) + self.get_anchor(dd_tokens, options) + end + + def handle_r_g_r(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[0], tokens[2]] + self.handle_r(new_tokens, options) + end + + # arrows + + def handle_srp(tokens, span, options) #:nodoc: + distance = tokens[0].get_tag(Scalar).type + repeater = tokens[1].get_tag(Repeater) + pointer = tokens[2].get_tag(Pointer).type + + repeater.offset(span, distance, pointer) + end + + def handle_s_r_p(tokens, options) #:nodoc: + repeater = tokens[1].get_tag(Repeater) + + # span = + # case true + # when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class) + # self.parse("this hour", :guess => false, :now => @now) + # when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class) + # self.parse("this minute", :guess => false, :now => @now) + # when [RepeaterMinute, RepeaterSecond].include?(repeater.class) + # self.parse("this second", :guess => false, :now => @now) + # else + # raise(ChronicPain, "Invalid repeater: #{repeater.class}") + # end + + span = self.parse("this second", :guess => false, :now => @now) + + self.handle_srp(tokens, span, options) + end + + def handle_p_s_r(tokens, options) #:nodoc: + new_tokens = [tokens[1], tokens[2], tokens[0]] + self.handle_s_r_p(new_tokens, options) + end + + def handle_s_r_p_a(tokens, options) #:nodoc: + anchor_span = get_anchor(tokens[3..tokens.size - 1], options) + self.handle_srp(tokens, anchor_span, options) + end + + # narrows + + def handle_orr(tokens, outer_span, options) #:nodoc: + repeater = tokens[1].get_tag(Repeater) + repeater.start = outer_span.begin - 1 + ordinal = tokens[0].get_tag(Ordinal).type + span = nil + ordinal.times do + span = repeater.next(:future) + if span.begin > outer_span.end + span = nil + break + end + end + span + end + + def handle_o_r_s_r(tokens, options) #:nodoc: + outer_span = get_anchor([tokens[3]], options) + handle_orr(tokens[0..1], outer_span, options) + end + + def handle_o_r_g_r(tokens, options) #:nodoc: + outer_span = get_anchor(tokens[2..3], options) + handle_orr(tokens[0..1], outer_span, options) + end + + # support methods + + def get_anchor(tokens, options) #:nodoc: + grabber = Grabber.new(:this) + pointer = :future + + repeaters = self.get_repeaters(tokens) + repeaters.size.times { tokens.pop } + + if tokens.first && tokens.first.get_tag(Grabber) + grabber = tokens.first.get_tag(Grabber) + tokens.pop + end + + head = repeaters.shift + head.start = @now + + case grabber.type + when :last + outer_span = head.next(:past) + when :this + if repeaters.size > 0 + outer_span = head.this(:none) + else + outer_span = head.this(options[:context]) + end + when :next + outer_span = head.next(:future) + else raise(ChronicPain, "Invalid grabber") + end + + puts "--#{outer_span}" if Chronic.debug + anchor = find_within(repeaters, outer_span, pointer) + end + + def get_repeaters(tokens) #:nodoc: + repeaters = [] + tokens.each do |token| + if t = token.get_tag(Repeater) + repeaters << t + end + end + repeaters.sort.reverse + end + + # Recursively finds repeaters within other repeaters. + # Returns a Span representing the innermost time span + # or nil if no repeater union could be found + def find_within(tags, span, pointer) #:nodoc: + puts "--#{span}" if Chronic.debug + return span if tags.empty? + + head, *rest = tags + head.start = pointer == :future ? span.begin : span.end + h = head.this(:none) + + if span.include?(h.begin) || span.include?(h.end) + return find_within(rest, h, pointer) + else + return nil + end + end + + def dealias_and_disambiguate_times(tokens, options) #:nodoc: + # handle aliases of am/pm + # 5:00 in the morning -> 5:00 am + # 7:00 in the evening -> 7:00 pm + + day_portion_index = nil + tokens.each_with_index do |t, i| + if t.get_tag(RepeaterDayPortion) + day_portion_index = i + break + end + end + + time_index = nil + tokens.each_with_index do |t, i| + if t.get_tag(RepeaterTime) + time_index = i + break + end + end + + if (day_portion_index && time_index) + t1 = tokens[day_portion_index] + t1tag = t1.get_tag(RepeaterDayPortion) + + if [:morning].include?(t1tag.type) + puts '--morning->am' if Chronic.debug + t1.untag(RepeaterDayPortion) + t1.tag(RepeaterDayPortion.new(:am)) + elsif [:afternoon, :evening, :night].include?(t1tag.type) + puts "--#{t1tag.type}->pm" if Chronic.debug + t1.untag(RepeaterDayPortion) + t1.tag(RepeaterDayPortion.new(:pm)) + end + end + + # tokens.each_with_index do |t0, i| + # t1 = tokens[i + 1] + # if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime) + # if [:morning].include?(t1tag.type) + # puts '--morning->am' if Chronic.debug + # t1.untag(RepeaterDayPortion) + # t1.tag(RepeaterDayPortion.new(:am)) + # elsif [:afternoon, :evening, :night].include?(t1tag.type) + # puts "--#{t1tag.type}->pm" if Chronic.debug + # t1.untag(RepeaterDayPortion) + # t1.tag(RepeaterDayPortion.new(:pm)) + # end + # end + # end + + # handle ambiguous times if :ambiguous_time_range is specified + if options[:ambiguous_time_range] != :none + ttokens = [] + tokens.each_with_index do |t0, i| + ttokens << t0 + t1 = tokens[i + 1] + if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion)) + distoken = Token.new('disambiguator') + distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range])) + ttokens << distoken + end + end + tokens = ttokens + end + + tokens + end + + end + + class Handler #:nodoc: + attr_accessor :pattern, :handler_method + + def initialize(pattern, handler_method) + @pattern = pattern + @handler_method = handler_method + end + + def constantize(name) + camel = name.to_s.gsub(/(^|_)(.)/) { $2.upcase } + ::Chronic.module_eval(camel, __FILE__, __LINE__) + end + + def match(tokens, definitions) + token_index = 0 + @pattern.each do |element| + name = element.to_s + optional = name.reverse[0..0] == '?' + name = name.chop if optional + if element.instance_of? Symbol + klass = constantize(name) + match = tokens[token_index] && !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty? + return false if !match && !optional + (token_index += 1; next) if match + next if !match && optional + elsif element.instance_of? String + return true if optional && token_index == tokens.size + sub_handlers = definitions[name.intern] || raise(ChronicPain, "Invalid subset #{name} specified") + sub_handlers.each do |sub_handler| + return true if sub_handler.match(tokens[token_index..tokens.size], definitions) + end + return false + else + raise(ChronicPain, "Invalid match type: #{element.class}") + end + end + return false if token_index != tokens.size + return true + end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Ordinal.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Ordinal.php new file mode 100644 index 000000000..45b8148e4 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Ordinal.php @@ -0,0 +1,40 @@ +module Chronic + + class Ordinal < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_ordinals(tokens[i]) then tokens[i].tag(t) end + if t = self.scan_for_days(tokens[i]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_ordinals(token) + if token.word =~ /^(\d*)(st|nd|rd|th)$/ + return Ordinal.new($1.to_i) + end + return nil + end + + def self.scan_for_days(token) + if token.word =~ /^(\d*)(st|nd|rd|th)$/ + unless $1.to_i > 31 + return OrdinalDay.new(token.word.to_i) + end + end + return nil + end + + def to_s + 'ordinal' + end + end + + class OrdinalDay < Ordinal #:nodoc: + def to_s + super << '-day-' << @type.to_s + end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Parser.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Parser.php new file mode 100644 index 000000000..5e7779f63 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Parser.php @@ -0,0 +1,239 @@ +module Chronic + class << self + + # Parses a string containing a natural language date or time. If the parser + # can find a date or time, either a Time or Chronic::Span will be returned + # (depending on the value of :guess). If no date or time can be found, + # +nil+ will be returned. + # + # Options are: + # + # [:context] + # :past or :future (defaults to :future) + # + # If your string represents a birthday, you can set :context to :past + # and if an ambiguous string is given, it will assume it is in the + # past. Specify :future or omit to set a future context. + # + # [:now] + # Time (defaults to Time.now) + # + # By setting :now to a Time, all computations will be based off + # of that time instead of Time.now + # + # [:guess] + # +true+ or +false+ (defaults to +true+) + # + # By default, the parser will guess a single point in time for the + # given date or time. If you'd rather have the entire time span returned, + # set :guess to +false+ and a Chronic::Span will be returned. + # + # [:ambiguous_time_range] + # Integer or :none (defaults to 6 (6am-6pm)) + # + # If an Integer is given, ambiguous times (like 5:00) will be + # assumed to be within the range of that time in the AM to that time + # in the PM. For example, if you set it to 7, then the parser will + # look for the time between 7am and 7pm. In the case of 5:00, it would + # assume that means 5:00pm. If :none is given, no assumption + # will be made, and the first matching instance of that time will + # be used. + def parse(text, specified_options = {}) + # get options and set defaults if necessary + default_options = {:context => :future, + :now => Time.now, + :guess => true, + :ambiguous_time_range => 6} + options = default_options.merge specified_options + + # ensure the specified options are valid + specified_options.keys.each do |key| + default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.") + end + [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.") + + # store now for later =) + @now = options[:now] + + # put the text into a normal format to ease scanning + text = self.pre_normalize(text) + + # get base tokens for each word + @tokens = self.base_tokenize(text) + + # scan the tokens with each token scanner + [Repeater].each do |tokenizer| + @tokens = tokenizer.scan(@tokens, options) + end + + [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer| + @tokens = tokenizer.scan(@tokens) + end + + # strip any non-tagged tokens + @tokens = @tokens.select { |token| token.tagged? } + + if Chronic.debug + puts "+---------------------------------------------------" + puts "| " + @tokens.to_s + puts "+---------------------------------------------------" + end + + # do the heavy lifting + begin + span = self.tokens_to_span(@tokens, options) + rescue + raise + return nil + end + + # guess a time within a span if required + if options[:guess] + return self.guess(span) + else + return span + end + end + + # Clean up the specified input text by stripping unwanted characters, + # converting idioms to their canonical form, converting number words + # to numbers (three => 3), and converting ordinal words to numeric + # ordinals (third => 3rd) + def pre_normalize(text) #:nodoc: + normalized_text = text.to_s.downcase + normalized_text = numericize_numbers(normalized_text) + normalized_text.gsub!(/['"\.]/, '') + normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' } + normalized_text.gsub!(/\btoday\b/, 'this day') + normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day') + normalized_text.gsub!(/\byesterday\b/, 'last day') + normalized_text.gsub!(/\bnoon\b/, '12:00') + normalized_text.gsub!(/\bmidnight\b/, '24:00') + normalized_text.gsub!(/\bbefore now\b/, 'past') + normalized_text.gsub!(/\bnow\b/, 'this second') + normalized_text.gsub!(/\b(ago|before)\b/, 'past') + normalized_text.gsub!(/\bthis past\b/, 'last') + normalized_text.gsub!(/\bthis last\b/, 'last') + normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1') + normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1') + normalized_text.gsub!(/\btonight\b/, 'this night') + normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1') + normalized_text.gsub!(/\b(hence|after|from)\b/, 'future') + normalized_text = numericize_ordinals(normalized_text) + end + + # Convert number words to numbers (three => 3) + def numericize_numbers(text) #:nodoc: + Numerizer.numerize(text) + end + + # Convert ordinal words to numeric ordinals (third => 3rd) + def numericize_ordinals(text) #:nodoc: + text + end + + # Split the text on spaces and convert each word into + # a Token + def base_tokenize(text) #:nodoc: + text.split(' ').map { |word| Token.new(word) } + end + + # Guess a specific time within the given span + def guess(span) #:nodoc: + return nil if span.nil? + if span.width > 1 + span.begin + (span.width / 2) + else + span.begin + end + end + end + + class Token #:nodoc: + attr_accessor :word, :tags + + def initialize(word) + @word = word + @tags = [] + end + + # Tag this token with the specified tag + def tag(new_tag) + @tags << new_tag + end + + # Remove all tags of the given class + def untag(tag_class) + @tags = @tags.select { |m| !m.kind_of? tag_class } + end + + # Return true if this token has any tags + def tagged? + @tags.size > 0 + end + + # Return the Tag that matches the given class + def get_tag(tag_class) + matches = @tags.select { |m| m.kind_of? tag_class } + #matches.size < 2 || raise("Multiple identical tags found") + return matches.first + end + + # Print this Token in a pretty way + def to_s + @word << '(' << @tags.join(', ') << ') ' + end + end + + # A Span represents a range of time. Since this class extends + # Range, you can use #begin and #end to get the beginning and + # ending times of the span (they will be of class Time) + class Span < Range + # Returns the width of this span in seconds + def width + (self.end - self.begin).to_i + end + + # Add a number of seconds to this span, returning the + # resulting Span + def +(seconds) + Span.new(self.begin + seconds, self.end + seconds) + end + + # Subtract a number of seconds to this span, returning the + # resulting Span + def -(seconds) + self + -seconds + end + + # Prints this span in a nice fashion + def to_s + '(' << self.begin.to_s << '..' << self.end.to_s << ')' + end + end + + # Tokens are tagged with subclassed instances of this class when + # they match specific criteria + class Tag #:nodoc: + attr_accessor :type + + def initialize(type) + @type = type + end + + def start=(s) + @now = s + end + end + + # Internal exception + class ChronicPain < Exception #:nodoc: + + end + + # This exception is raised if an invalid argument is provided to + # any of Chronic's methods + class InvalidArgumentException < Exception + + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Pointer.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Pointer.php new file mode 100644 index 000000000..224efaf96 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Pointer.php @@ -0,0 +1,27 @@ +module Chronic + + class Pointer < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/\bpast\b/ => :past, + /\bfuture\b/ => :future, + /\bin\b/ => :future} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'pointer-' << @type.to_s + end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeater.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeater.php new file mode 100644 index 000000000..9f80daf2f --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeater.php @@ -0,0 +1,115 @@ +class Chronic::Repeater < Chronic::Tag #:nodoc: + def self.scan(tokens, options) + # for each token + tokens.each_index do |i| + if t = self.scan_for_month_names(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_day_names(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_day_portions(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_times(tokens[i], options) then tokens[i].tag(t); next end + if t = self.scan_for_units(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_month_names(token) + scanner = {/^jan\.?(uary)?$/ => :january, + /^feb\.?(ruary)?$/ => :february, + /^mar\.?(ch)?$/ => :march, + /^apr\.?(il)?$/ => :april, + /^may$/ => :may, + /^jun\.?e?$/ => :june, + /^jul\.?y?$/ => :july, + /^aug\.?(ust)?$/ => :august, + /^sep\.?(t\.?|tember)?$/ => :september, + /^oct\.?(ober)?$/ => :october, + /^nov\.?(ember)?$/ => :november, + /^dec\.?(ember)?$/ => :december} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterMonthName.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_day_names(token) + scanner = {/^m[ou]n(day)?$/ => :monday, + /^t(ue|eu|oo|u|)s(day)?$/ => :tuesday, + /^tue$/ => :tuesday, + /^we(dnes|nds|nns)day$/ => :wednesday, + /^wed$/ => :wednesday, + /^th(urs|ers)day$/ => :thursday, + /^thu$/ => :thursday, + /^fr[iy](day)?$/ => :friday, + /^sat(t?[ue]rday)?$/ => :saturday, + /^su[nm](day)?$/ => :sunday} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterDayName.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_day_portions(token) + scanner = {/^ams?$/ => :am, + /^pms?$/ => :pm, + /^mornings?$/ => :morning, + /^afternoons?$/ => :afternoon, + /^evenings?$/ => :evening, + /^(night|nite)s?$/ => :night} + scanner.keys.each do |scanner_item| + return Chronic::RepeaterDayPortion.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_times(token, options) + if token.word =~ /^\d{1,2}(:?\d{2})?([\.:]?\d{2})?$/ + return Chronic::RepeaterTime.new(token.word, options) + end + return nil + end + + def self.scan_for_units(token) + scanner = {/^years?$/ => :year, + /^seasons?$/ => :season, + /^months?$/ => :month, + /^fortnights?$/ => :fortnight, + /^weeks?$/ => :week, + /^weekends?$/ => :weekend, + /^days?$/ => :day, + /^hours?$/ => :hour, + /^minutes?$/ => :minute, + /^seconds?$/ => :second} + scanner.keys.each do |scanner_item| + if scanner_item =~ token.word + klass_name = 'Chronic::Repeater' + scanner[scanner_item].to_s.capitalize + klass = eval(klass_name) + return klass.new(scanner[scanner_item]) + end + end + return nil + end + + def <=>(other) + width <=> other.width + end + + # returns the width (in seconds or months) of this repeatable. + def width + raise("Repeatable#width must be overridden in subclasses") + end + + # returns the next occurance of this repeatable. + def next(pointer) + !@now.nil? || raise("Start point must be set before calling #next") + [:future, :none, :past].include?(pointer) || raise("First argument 'pointer' must be one of :past or :future") + #raise("Repeatable#next must be overridden in subclasses") + end + + def this(pointer) + !@now.nil? || raise("Start point must be set before calling #this") + [:future, :past, :none].include?(pointer) || raise("First argument 'pointer' must be one of :past, :future, :none") + end + + def to_s + 'repeater' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Day.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Day.php new file mode 100644 index 000000000..a92d83f63 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Day.php @@ -0,0 +1,47 @@ +class Chronic::RepeaterDay < Chronic::Repeater #:nodoc: + DAY_SECONDS = 86_400 # (24 * 60 * 60) + + def next(pointer) + super + + if !@current_day_start + @current_day_start = Time.local(@now.year, @now.month, @now.day) + end + + direction = pointer == :future ? 1 : -1 + @current_day_start += direction * DAY_SECONDS + + Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + day_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS + when :past + day_begin = Time.construct(@now.year, @now.month, @now.day) + day_end = Time.construct(@now.year, @now.month, @now.day, @now.hour) + when :none + day_begin = Time.construct(@now.year, @now.month, @now.day) + day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS + end + + Chronic::Span.new(day_begin, day_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * DAY_SECONDS + end + + def width + DAY_SECONDS + end + + def to_s + super << '-day' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayName.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayName.php new file mode 100644 index 000000000..0486a4ddf --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayName.php @@ -0,0 +1,46 @@ +class Chronic::RepeaterDayName < Chronic::Repeater #:nodoc: + DAY_SECONDS = 86400 # (24 * 60 * 60) + + def next(pointer) + super + + direction = pointer == :future ? 1 : -1 + + if !@current_day_start + @current_day_start = Time.construct(@now.year, @now.month, @now.day) + @current_day_start += direction * DAY_SECONDS + + day_num = symbol_to_number(@type) + + while @current_day_start.wday != day_num + @current_day_start += direction * DAY_SECONDS + end + else + @current_day_start += direction * 7 * DAY_SECONDS + end + + Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS) + end + + def this(pointer = :future) + super + + pointer = :future if pointer == :none + self.next(pointer) + end + + def width + DAY_SECONDS + end + + def to_s + super << '-dayname-' << @type.to_s + end + + private + + def symbol_to_number(sym) + lookup = {:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6} + lookup[sym] || raise("Invalid symbol specified") + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayPortion.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayPortion.php new file mode 100644 index 000000000..c854933ad --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayPortion.php @@ -0,0 +1,93 @@ +class Chronic::RepeaterDayPortion < Chronic::Repeater #:nodoc: + @@morning = (6 * 60 * 60)..(12 * 60 * 60) # 6am-12am + @@afternoon = (13 * 60 * 60)..(17 * 60 * 60) # 1pm-5pm + @@evening = (17 * 60 * 60)..(20 * 60 * 60) # 5pm-8pm + @@night = (20 * 60 * 60)..(24 * 60 * 60) # 8pm-12pm + + def initialize(type) + super + + if type.kind_of? Integer + @range = (@type * 60 * 60)..((@type + 12) * 60 * 60) + else + lookup = {:am => 0..(12 * 60 * 60 - 1), + :pm => (12 * 60 * 60)..(24 * 60 * 60 - 1), + :morning => @@morning, + :afternoon => @@afternoon, + :evening => @@evening, + :night => @@night} + @range = lookup[type] + lookup[type] || raise("Invalid type '#{type}' for RepeaterDayPortion") + end + @range || raise("Range should have been set by now") + end + + def next(pointer) + super + + full_day = 60 * 60 * 24 + + if !@current_span + now_seconds = @now - Time.construct(@now.year, @now.month, @now.day) + if now_seconds < @range.begin + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin + end + elsif now_seconds > @range.end + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + end + else + case pointer + when :future + range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin + when :past + range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin + end + end + + @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin)) + else + case pointer + when :future + @current_span += full_day + when :past + @current_span -= full_day + end + end + end + + def this(context = :future) + super + + range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin + @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin)) + end + + def offset(span, amount, pointer) + @now = span.begin + portion_span = self.next(pointer) + direction = pointer == :future ? 1 : -1 + portion_span + (direction * (amount - 1) * Chronic::RepeaterDay::DAY_SECONDS) + end + + def width + @range || raise("Range has not been set") + return @current_span.width if @current_span + if @type.kind_of? Integer + return (12 * 60 * 60) + else + @range.end - @range.begin + end + end + + def to_s + super << '-dayportion-' << @type.to_s + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Fortnight.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Fortnight.php new file mode 100644 index 000000000..058fbb904 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Fortnight.php @@ -0,0 +1,65 @@ +class Chronic::RepeaterFortnight < Chronic::Repeater #:nodoc: + FORTNIGHT_SECONDS = 1_209_600 # (14 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_fortnight_start + case pointer + when :future + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + next_sunday_span = sunday_repeater.next(:future) + @current_fortnight_start = next_sunday_span.begin + when :past + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS) + 2.times { sunday_repeater.next(:past) } + last_sunday_span = sunday_repeater.next(:past) + @current_fortnight_start = last_sunday_span.begin + end + else + direction = pointer == :future ? 1 : -1 + @current_fortnight_start += direction * FORTNIGHT_SECONDS + end + + Chronic::Span.new(@current_fortnight_start, @current_fortnight_start + FORTNIGHT_SECONDS) + end + + def this(pointer = :future) + super + + pointer = :future if pointer == :none + + case pointer + when :future + this_fortnight_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + sunday_repeater.this(:future) + this_sunday_span = sunday_repeater.this(:future) + this_fortnight_end = this_sunday_span.begin + Chronic::Span.new(this_fortnight_start, this_fortnight_end) + when :past + this_fortnight_end = Time.construct(@now.year, @now.month, @now.day, @now.hour) + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_fortnight_start = last_sunday_span.begin + Chronic::Span.new(this_fortnight_start, this_fortnight_end) + end + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * FORTNIGHT_SECONDS + end + + def width + FORTNIGHT_SECONDS + end + + def to_s + super << '-fortnight' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Hour.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Hour.php new file mode 100644 index 000000000..f38a3f825 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Hour.php @@ -0,0 +1,52 @@ +class Chronic::RepeaterHour < Chronic::Repeater #:nodoc: + HOUR_SECONDS = 3600 # 60 * 60 + + def next(pointer) + super + + if !@current_hour_start + case pointer + when :future + @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + when :past + @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour - 1) + end + else + direction = pointer == :future ? 1 : -1 + @current_hour_start += direction * HOUR_SECONDS + end + + Chronic::Span.new(@current_hour_start, @current_hour_start + HOUR_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1) + hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1) + when :past + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + when :none + hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + hour_end = hour_begin + HOUR_SECONDS + end + + Chronic::Span.new(hour_start, hour_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * HOUR_SECONDS + end + + def width + HOUR_SECONDS + end + + def to_s + super << '-hour' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Minute.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Minute.php new file mode 100644 index 000000000..342d3cd41 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Minute.php @@ -0,0 +1,52 @@ +class Chronic::RepeaterMinute < Chronic::Repeater #:nodoc: + MINUTE_SECONDS = 60 + + def next(pointer = :future) + super + + if !@current_minute_start + case pointer + when :future + @current_minute_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1) + when :past + @current_minute_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min - 1) + end + else + direction = pointer == :future ? 1 : -1 + @current_minute_start += direction * MINUTE_SECONDS + end + + Chronic::Span.new(@current_minute_start, @current_minute_start + MINUTE_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + minute_begin = @now + minute_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + when :past + minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + minute_end = @now + when :none + minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + minute_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + MINUTE_SECONDS + end + + Chronic::Span.new(minute_begin, minute_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * MINUTE_SECONDS + end + + def width + MINUTE_SECONDS + end + + def to_s + super << '-minute' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Month.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Month.php new file mode 100644 index 000000000..edd89eeb2 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Month.php @@ -0,0 +1,61 @@ +class Chronic::RepeaterMonth < Chronic::Repeater #:nodoc: + MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60 + YEAR_MONTHS = 12 + + def next(pointer) + super + + if !@current_month_start + @current_month_start = offset_by(Time.construct(@now.year, @now.month), 1, pointer) + else + @current_month_start = offset_by(Time.construct(@current_month_start.year, @current_month_start.month), 1, pointer) + end + + Chronic::Span.new(@current_month_start, Time.construct(@current_month_start.year, @current_month_start.month + 1)) + end + + def this(pointer = :future) + super + + case pointer + when :future + month_start = Time.construct(@now.year, @now.month, @now.day + 1) + month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future) + when :past + month_start = Time.construct(@now.year, @now.month) + month_end = Time.construct(@now.year, @now.month, @now.day) + when :none + month_start = Time.construct(@now.year, @now.month) + month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future) + end + + Chronic::Span.new(month_start, month_end) + end + + def offset(span, amount, pointer) + Chronic::Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer)) + end + + def offset_by(time, amount, pointer) + direction = pointer == :future ? 1 : -1 + + amount_years = direction * amount / YEAR_MONTHS + amount_months = direction * amount % YEAR_MONTHS + + new_year = time.year + amount_years + new_month = time.month + amount_months + if new_month > YEAR_MONTHS + new_year += 1 + new_month -= YEAR_MONTHS + end + Time.construct(new_year, new_month, time.day, time.hour, time.min, time.sec) + end + + def width + MONTH_SECONDS + end + + def to_s + super << '-month' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/MonthName.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/MonthName.php new file mode 100644 index 000000000..1f8b748a9 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/MonthName.php @@ -0,0 +1,93 @@ +class Chronic::RepeaterMonthName < Chronic::Repeater #:nodoc: + MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60 + + def next(pointer) + super + + if !@current_month_begin + target_month = symbol_to_number(@type) + case pointer + when :future + if @now.month < target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month > target_month + @current_month_begin = Time.construct(@now.year + 1, target_month) + end + when :none + if @now.month <= target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month > target_month + @current_month_begin = Time.construct(@now.year + 1, target_month) + end + when :past + if @now.month > target_month + @current_month_begin = Time.construct(@now.year, target_month) + else @now.month < target_month + @current_month_begin = Time.construct(@now.year - 1, target_month) + end + end + @current_month_begin || raise("Current month should be set by now") + else + case pointer + when :future + @current_month_begin = Time.construct(@current_month_begin.year + 1, @current_month_begin.month) + when :past + @current_month_begin = Time.construct(@current_month_begin.year - 1, @current_month_begin.month) + end + end + + cur_month_year = @current_month_begin.year + cur_month_month = @current_month_begin.month + + if cur_month_month == 12 + next_month_year = cur_month_year + 1 + next_month_month = 1 + else + next_month_year = cur_month_year + next_month_month = cur_month_month + 1 + end + + Chronic::Span.new(@current_month_begin, Time.construct(next_month_year, next_month_month)) + end + + def this(pointer = :future) + super + + case pointer + when :past + self.next(pointer) + when :future, :none + self.next(:none) + end + end + + def width + MONTH_SECONDS + end + + def index + symbol_to_number(@type) + end + + def to_s + super << '-monthname-' << @type.to_s + end + + private + + def symbol_to_number(sym) + lookup = {:january => 1, + :february => 2, + :march => 3, + :april => 4, + :may => 5, + :june => 6, + :july => 7, + :august => 8, + :september => 9, + :october => 10, + :november => 11, + :december => 12} + lookup[sym] || raise("Invalid symbol specified") + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Season.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Season.php new file mode 100644 index 000000000..a255865fb --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Season.php @@ -0,0 +1,23 @@ +class Chronic::RepeaterSeason < Chronic::Repeater #:nodoc: + SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60 + + def next(pointer) + super + + raise 'Not implemented' + end + + def this(pointer = :future) + super + + raise 'Not implemented' + end + + def width + SEASON_SECONDS + end + + def to_s + super << '-season' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/SeasonName.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/SeasonName.php new file mode 100644 index 000000000..adfd1f281 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/SeasonName.php @@ -0,0 +1,24 @@ +class Chronic::RepeaterSeasonName < Chronic::RepeaterSeason #:nodoc: + @summer = ['jul 21', 'sep 22'] + @autumn = ['sep 23', 'dec 21'] + @winter = ['dec 22', 'mar 19'] + @spring = ['mar 20', 'jul 20'] + + def next(pointer) + super + raise 'Not implemented' + end + + def this(pointer = :future) + super + raise 'Not implemented' + end + + def width + (91 * 24 * 60 * 60) + end + + def to_s + super << '-season-' << @type.to_s + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Second.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Second.php new file mode 100644 index 000000000..6d05545ca --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Second.php @@ -0,0 +1,36 @@ +class Chronic::RepeaterSecond < Chronic::Repeater #:nodoc: + SECOND_SECONDS = 1 # haha, awesome + + def next(pointer = :future) + super + + direction = pointer == :future ? 1 : -1 + + if !@second_start + @second_start = @now + (direction * SECOND_SECONDS) + else + @second_start += SECOND_SECONDS * direction + end + + Chronic::Span.new(@second_start, @second_start + SECOND_SECONDS) + end + + def this(pointer = :future) + super + + Chronic::Span.new(@now, @now + 1) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * SECOND_SECONDS + end + + def width + SECOND_SECONDS + end + + def to_s + super << '-second' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Time.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Time.php new file mode 100644 index 000000000..f8560141c --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Time.php @@ -0,0 +1,117 @@ +class Chronic::RepeaterTime < Chronic::Repeater #:nodoc: + class Tick #:nodoc: + attr_accessor :time + + def initialize(time, ambiguous = false) + @time = time + @ambiguous = ambiguous + end + + def ambiguous? + @ambiguous + end + + def *(other) + Tick.new(@time * other, @ambiguous) + end + + def to_f + @time.to_f + end + + def to_s + @time.to_s + (@ambiguous ? '?' : '') + end + end + + def initialize(time, options = {}) + t = time.gsub(/\:/, '') + @type = + if (1..2) === t.size + hours = t.to_i + hours == 12 ? Tick.new(0 * 60 * 60, true) : Tick.new(hours * 60 * 60, true) + elsif t.size == 3 + Tick.new((t[0..0].to_i * 60 * 60) + (t[1..2].to_i * 60), true) + elsif t.size == 4 + ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12 + hours = t[0..1].to_i + hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60, ambiguous) + elsif t.size == 5 + Tick.new(t[0..0].to_i * 60 * 60 + t[1..2].to_i * 60 + t[3..4].to_i, true) + elsif t.size == 6 + ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12 + hours = t[0..1].to_i + hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous) + else + raise("Time cannot exceed six digits") + end + end + + # Return the next past or future Span for the time that this Repeater represents + # pointer - Symbol representing which temporal direction to fetch the next day + # must be either :past or :future + def next(pointer) + super + + half_day = 60 * 60 * 12 + full_day = 60 * 60 * 24 + + first = false + + unless @current_time + first = true + midnight = Time.local(@now.year, @now.month, @now.day) + yesterday_midnight = midnight - full_day + tomorrow_midnight = midnight + full_day + + catch :done do + if pointer == :future + if @type.ambiguous? + [midnight + @type, midnight + half_day + @type, tomorrow_midnight + @type].each do |t| + (@current_time = t; throw :done) if t >= @now + end + else + [midnight + @type, tomorrow_midnight + @type].each do |t| + (@current_time = t; throw :done) if t >= @now + end + end + else # pointer == :past + if @type.ambiguous? + [midnight + half_day + @type, midnight + @type, yesterday_midnight + @type * 2].each do |t| + (@current_time = t; throw :done) if t <= @now + end + else + [midnight + @type, yesterday_midnight + @type].each do |t| + (@current_time = t; throw :done) if t <= @now + end + end + end + end + + @current_time || raise("Current time cannot be nil at this point") + end + + unless first + increment = @type.ambiguous? ? half_day : full_day + @current_time += pointer == :future ? increment : -increment + end + + Chronic::Span.new(@current_time, @current_time + width) + end + + def this(context = :future) + super + + context = :future if context == :none + + self.next(context) + end + + def width + 1 + end + + def to_s + super << '-time-' << @type.to_s + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Week.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Week.php new file mode 100644 index 000000000..ec88ff14b --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Week.php @@ -0,0 +1,68 @@ +class Chronic::RepeaterWeek < Chronic::Repeater #:nodoc: + WEEK_SECONDS = 604800 # (7 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_week_start + case pointer + when :future + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + next_sunday_span = sunday_repeater.next(:future) + @current_week_start = next_sunday_span.begin + when :past + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS) + sunday_repeater.next(:past) + last_sunday_span = sunday_repeater.next(:past) + @current_week_start = last_sunday_span.begin + end + else + direction = pointer == :future ? 1 : -1 + @current_week_start += direction * WEEK_SECONDS + end + + Chronic::Span.new(@current_week_start, @current_week_start + WEEK_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future + this_week_start = Time.local(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + this_sunday_span = sunday_repeater.this(:future) + this_week_end = this_sunday_span.begin + Chronic::Span.new(this_week_start, this_week_end) + when :past + this_week_end = Time.local(@now.year, @now.month, @now.day, @now.hour) + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_week_start = last_sunday_span.begin + Chronic::Span.new(this_week_start, this_week_end) + when :none + sunday_repeater = Chronic::RepeaterDayName.new(:sunday) + sunday_repeater.start = @now + last_sunday_span = sunday_repeater.next(:past) + this_week_start = last_sunday_span.begin + Chronic::Span.new(this_week_start, this_week_start + WEEK_SECONDS) + end + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + span + direction * amount * WEEK_SECONDS + end + + def width + WEEK_SECONDS + end + + def to_s + super << '-week' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Weekend.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Weekend.php new file mode 100644 index 000000000..f012267d9 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Weekend.php @@ -0,0 +1,60 @@ +class Chronic::RepeaterWeekend < Chronic::Repeater #:nodoc: + WEEKEND_SECONDS = 172_800 # (2 * 24 * 60 * 60) + + def next(pointer) + super + + if !@current_week_start + case pointer + when :future + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + next_saturday_span = saturday_repeater.next(:future) + @current_week_start = next_saturday_span.begin + when :past + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS) + last_saturday_span = saturday_repeater.next(:past) + @current_week_start = last_saturday_span.begin + end + else + direction = pointer == :future ? 1 : -1 + @current_week_start += direction * Chronic::RepeaterWeek::WEEK_SECONDS + end + + Chronic::Span.new(@current_week_start, @current_week_start + WEEKEND_SECONDS) + end + + def this(pointer = :future) + super + + case pointer + when :future, :none + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + this_saturday_span = saturday_repeater.this(:future) + Chronic::Span.new(this_saturday_span.begin, this_saturday_span.begin + WEEKEND_SECONDS) + when :past + saturday_repeater = Chronic::RepeaterDayName.new(:saturday) + saturday_repeater.start = @now + last_saturday_span = saturday_repeater.this(:past) + Chronic::Span.new(last_saturday_span.begin, last_saturday_span.begin + WEEKEND_SECONDS) + end + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + weekend = Chronic::RepeaterWeekend.new(:weekend) + weekend.start = span.begin + start = weekend.next(pointer).begin + (amount - 1) * direction * Chronic::RepeaterWeek::WEEK_SECONDS + Chronic::Span.new(start, start + (span.end - span.begin)) + end + + def width + WEEKEND_SECONDS + end + + def to_s + super << '-weekend' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Year.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Year.php new file mode 100644 index 000000000..426371f9b --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Year.php @@ -0,0 +1,58 @@ +class Chronic::RepeaterYear < Chronic::Repeater #:nodoc: + + def next(pointer) + super + + if !@current_year_start + case pointer + when :future + @current_year_start = Time.construct(@now.year + 1) + when :past + @current_year_start = Time.construct(@now.year - 1) + end + else + diff = pointer == :future ? 1 : -1 + @current_year_start = Time.construct(@current_year_start.year + diff) + end + + Chronic::Span.new(@current_year_start, Time.construct(@current_year_start.year + 1)) + end + + def this(pointer = :future) + super + + case pointer + when :future + this_year_start = Time.construct(@now.year, @now.month, @now.day) + Chronic::RepeaterDay::DAY_SECONDS + this_year_end = Time.construct(@now.year + 1, 1, 1) + when :past + this_year_start = Time.construct(@now.year, 1, 1) + this_year_end = Time.construct(@now.year, @now.month, @now.day) + when :none + this_year_start = Time.construct(@now.year, 1, 1) + this_year_end = Time.construct(@now.year + 1, 1, 1) + end + + Chronic::Span.new(this_year_start, this_year_end) + end + + def offset(span, amount, pointer) + direction = pointer == :future ? 1 : -1 + + sb = span.begin + new_begin = Time.construct(sb.year + (amount * direction), sb.month, sb.day, sb.hour, sb.min, sb.sec) + + se = span.end + new_end = Time.construct(se.year + (amount * direction), se.month, se.day, se.hour, se.min, se.sec) + + Chronic::Span.new(new_begin, new_end) + end + + def width + (365 * 24 * 60 * 60) + end + + def to_s + super << '-year' + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Scalar.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Scalar.php new file mode 100644 index 000000000..b08cfee18 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Scalar.php @@ -0,0 +1,74 @@ +module Chronic + + class Scalar < Tag #:nodoc: + def self.scan(tokens) + # for each token + tokens.each_index do |i| + if t = self.scan_for_scalars(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_days(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_months(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + if t = self.scan_for_years(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end + end + tokens + end + + def self.scan_for_scalars(token, post_token) + if token.word =~ /^\d*$/ + unless post_token && %w{am pm morning afternoon evening night}.include?(post_token) + return Scalar.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_days(token, post_token) + if token.word =~ /^\d\d?$/ + unless token.word.to_i > 31 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token)) + return ScalarDay.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_months(token, post_token) + if token.word =~ /^\d\d?$/ + unless token.word.to_i > 12 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token)) + return ScalarMonth.new(token.word.to_i) + end + end + return nil + end + + def self.scan_for_years(token, post_token) + if token.word =~ /^([1-9]\d)?\d\d?$/ + unless post_token && %w{am pm morning afternoon evening night}.include?(post_token) + return ScalarYear.new(token.word.to_i) + end + end + return nil + end + + def to_s + 'scalar' + end + end + + class ScalarDay < Scalar #:nodoc: + def to_s + super << '-day-' << @type.to_s + end + end + + class ScalarMonth < Scalar #:nodoc: + def to_s + super << '-month-' << @type.to_s + end + end + + class ScalarYear < Scalar #:nodoc: + def to_s + super << '-year-' << @type.to_s + end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Separator.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Separator.php new file mode 100644 index 000000000..86c56e33b --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Separator.php @@ -0,0 +1,76 @@ +module Chronic + + class Separator < Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_commas(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_slash_or_dash(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_at(tokens[i]) then tokens[i].tag(t); next end + if t = self.scan_for_in(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_commas(token) + scanner = {/^,$/ => :comma} + scanner.keys.each do |scanner_item| + return SeparatorComma.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_slash_or_dash(token) + scanner = {/^-$/ => :dash, + /^\/$/ => :slash} + scanner.keys.each do |scanner_item| + return SeparatorSlashOrDash.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_at(token) + scanner = {/^(at|@)$/ => :at} + scanner.keys.each do |scanner_item| + return SeparatorAt.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def self.scan_for_in(token) + scanner = {/^in$/ => :in} + scanner.keys.each do |scanner_item| + return SeparatorIn.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'separator' + end + end + + class SeparatorComma < Separator #:nodoc: + def to_s + super << '-comma' + end + end + + class SeparatorSlashOrDash < Separator #:nodoc: + def to_s + super << '-slashordash-' << @type.to_s + end + end + + class SeparatorAt < Separator #:nodoc: + def to_s + super << '-at' + end + end + + class SeparatorIn < Separator #:nodoc: + def to_s + super << '-in' + end + end + +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/TimeZone.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/TimeZone.php new file mode 100644 index 000000000..41041ef47 --- /dev/null +++ b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/TimeZone.php @@ -0,0 +1,22 @@ +module Chronic + class TimeZone < Tag #:nodoc: + def self.scan(tokens) + tokens.each_index do |i| + if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end + end + tokens + end + + def self.scan_for_all(token) + scanner = {/[PMCE][DS]T/i => :tz} + scanner.keys.each do |scanner_item| + return self.new(scanner[scanner_item]) if scanner_item =~ token.word + end + return nil + end + + def to_s + 'timezone' + end + end +end \ No newline at end of file diff --git a/framework/Horde_Date_Parser/package.xml b/framework/Horde_Date_Parser/package.xml new file mode 100644 index 000000000..e69de29bb diff --git a/framework/Horde_Date_Parser/test/Horde/Date/Parser/AllTests.php b/framework/Horde_Date_Parser/test/Horde/Date/Parser/AllTests.php new file mode 100644 index 000000000..7fac8b189 --- /dev/null +++ b/framework/Horde_Date_Parser/test/Horde/Date/Parser/AllTests.php @@ -0,0 +1,54 @@ +isFile() && preg_match('/Test.php$/', $file->getFilename())) { + $pathname = $file->getPathname(); + require $pathname; + + $class = str_replace(DIRECTORY_SEPARATOR, '_', + preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname)); + $suite->addTestSuite('Horde_Support_' . $class); + } + } + + return $suite; + } + +} + +if (PHPUnit_MAIN_METHOD == 'Horde_Support_AllTests::main') { + Horde_Support_AllTests::main(); +} diff --git a/hippo/SPEC.txt b/hippo/SPEC.txt new file mode 100644 index 000000000..37d3fdfa8 --- /dev/null +++ b/hippo/SPEC.txt @@ -0,0 +1,10 @@ +Hippo will be a new Horde 4, PHP 5 application that aggregates content. It will initially be written to aggregate feeds, both internal from Jonah natively, and external (through Horde_Feed). Hippo will work with Horde_Content types and have Input and Output classes. Input classes will allow reading a content type, such as Horde_Feed feeds, Jonah internal feeds, comics, etc. Output classes will allow outputting a "remixed" feed from one or more inputs (such as combining an RSS feed with a few comics into a daily feed). + + * There will be no direct storage of inputs or outputs, but Horde_Cache will be used. + * Some content types such as comics might include code to fetch and store themselves locally; we might decide that Horde_Feed external feeds should be fetched into a local content table also + * This application should be able to provide the backend for Planet Horde (http://planet.horde.org/) + * Fields to aggregate for feeds: title, url, uid, summary, content if any, content-type for content, date + * During Hippo runs, Horde_Log will be used for logging + * There will be the ability to run filters on Hippo_Input plugins. For instance, if a certain feed has the same junk in every entry, a filter could be attached to the Hippo_Input for that feed that would strip it out. Generic (regex-type) filters should be configurable via the UI; it should be possible to configure any kind of filter in a config file or separate input plugin class. + * This application should eventually be able to replace Klutz + * Additional possible content types include podcasts, content from other Horde applications (iCalendar data from Kronolith), "friend information (http://hasin.wordpress.com/2008/05/31/building-friendfeed-using-php-part-1/, http://hasin.wordpress.com/2008/06/03/building-services-like-friendfeed-using-php-part2/), etc. diff --git a/hippo/TODO.txt b/hippo/TODO.txt new file mode 100644 index 000000000..e69de29bb diff --git a/hippo/planet.horde.org/.htaccess b/hippo/planet.horde.org/.htaccess new file mode 100644 index 000000000..c5c8612cc --- /dev/null +++ b/hippo/planet.horde.org/.htaccess @@ -0,0 +1,26 @@ +# The following php_* instruction only work, if you're running +# php as apache-module, not as cgi. +# they are not really needed... +#php_value error_reporting 2039 +#php_flag display_errors on +#php_value magic_quotes_gpc off + +RewriteEngine On + +RewriteCond %{REQUEST_URI} !^/*themes/planet-horde/.* +RewriteRule ^/*themes/(.+)$ ./themes/planet-horde/$1 [L] + +RewriteCond %{REQUEST_URI} !^/*themes/planet-horde/.* +RewriteCond %{REQUEST_URI} !^/*index.php +RewriteCond %{REQUEST_URI} !^.*info.php +RewriteCond %{REQUEST_URI} !^.*test.php +RewriteCond %{REQUEST_URI} !^.*js/ +RewriteCond %{REQUEST_URI} !^.*images/ +RewriteCond %{REQUEST_URI} !^.*robots.txt +RewriteCond %{REQUEST_URI} !^/*s9y/ +RewriteCond %{REQUEST_URI} !^.*admin/ +RewriteCond %{REQUEST_URI} !^.*_vti_.* +RewriteCond %{REQUEST_URI} !favicon.ico +RewriteCond %{REQUEST_URI} !search.html +RewriteRule ^(.+)$ ./index.php?path=$1 [QSA] + diff --git a/hippo/planet.horde.org/favicon.ico b/hippo/planet.horde.org/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..461907c719c7473af282b1041d13e22ca3488674 GIT binary patch literal 1150 zcmb_cTSyd982-J~wmX+|=CZS`y10UEnRy|_TNYV{-84&~vIijw*Rry(=$J2 zG9~LR2_qs3G>ORW8A9*wLk0N|L=Oc?$vORJWnc(Fg3io8|C#^)zW+9d0f@tkQov^r z!ifMD04zg66Iy2Ns9-Jn{&g9qS*@z-(M7oC<08)MwX8M;m%zH+?gVw)nvY8BUgq(5 zmdwXDd{(!1=Nu=6Srevd26bIONN=9zYUg2&{5+eF;pgmxQkti>H*oV=FOv?{GIFJv znQyOiZRjkc!IWDVuTswn-IZ45Od-~9$FnuYjZ-$2eD;nHa3T7`i(J3g!9@&Xw40M_ z$76eWf7K7HFW2TtlC%@+JAnOPr2{8p`PGfZQI)7+j9E{Mvd4|#%Yw_4GxfhM%i3&n zWm)!n!qwb()Wyx0eX+dqzN+tDuQ$noajRQOqoO8dYiZQB3+-~bBz>{3RjDbP(r&jg zfze8GN1Yh4my>)wB0ss*#EkUR8=9u&I5F76Lb|@-z2K9|)sy(`;+=lFGGkcygorw& zrW%I*mfINY-$e>mOvLgJMwpi3??FGdCoiy){4J#G2s2;zGpV-h3#llcP;-_(L4QE- zv4?mqlr|*hEoLo5zki;~jXQqfET*-=$n0E>?V2$RBg4*VYeGn>w0px(`NWSHk^%5ZmHwutA4Izhu$IQlu}pm z)qLK|wX{H=lQ-8MXOSp1Zf9yom|?5}fx+{#Tq(+$^aYZe=i>ZrquHSsG&B9N(~h%@ wbH4wNJx>Mrgz_O71)~dzB;Y@z10Z1xVC*RX-Vkxk${Wt6aWAK literal 0 HcmV?d00001 diff --git a/hippo/planet.horde.org/libs/aggregator.php b/hippo/planet.horde.org/libs/aggregator.php new file mode 100644 index 000000000..1f5656717 --- /dev/null +++ b/hippo/planet.horde.org/libs/aggregator.php @@ -0,0 +1,310 @@ +mdb = MDB2::connect($GLOBALS['BX_config']['dsn']); + if(MDB2::isError($this->mdb)) { + die('unable to connect to db'); + } + } + + function aggregateAllBlogs($id = null) { + $where = ''; + if ($id) { + $where = "where ID = $id"; + } + $res = $this->mdb->query("select ID,blogsID as blogsid, link, cats, section from feeds $where"); + if (MDB2::isError($res)) { + print $res->getMessage(); + print "\n"; + print $res->getUserinfo(); + die(); + } + while ($row = $res->fetchRow(MDB2_FETCHMODE_ASSOC)) { + //get remote feed from magpie + $feed = $this->getRemoteFeed($row['link']); + if(!$feed) { + continue; + } + //check if this blog already exists + if (!$feed->channel['link']) { + print "NO channel/link... PLEASE FIX THIS\n"; + continue; + } + + $blog = $this->getBlogEntry($feed->channel['link']); + $blog = $this->mdb->queryRow ("select * from blogs where id = ".(int)$row['blogsid'], null,MDB2_FETCHMODE_ASSOC); + $newBlog = false; + if (!$blog) { + // $id = $this->insertBlogEntry($feed->channel); + // print "new Blog: " .$feed->channel['title'] ."\n"; + // $newBlog = true; + } else { + //TODO: check for changed channel entries + $id = $blog['id']; + if ($feed->channel['title'] && $blog['title'] != $feed->channel['title'] && $row['section'] != 'comments') { + // $this->updateBlogEntry($feed->channel, $id); + } + } + // update id, if not the same + if ($row['blogsid'] != $id) { + // $this->updateFeedBlogID($row['link'], $id); + } + + //loop through feeds + + foreach ($feed->items as $item) { + if (isset($item['guid'])) { + $guid = $item['guid']; + } else if (isset($item['id'])) { + $guid = $item['id']; + $item['guid'] = $item['id']; + } else { + $guid = $item['link']; + $item['guid'] = $item['link']; + } + + if (!isset($item['content']['encoded']) && isset($item['atom_content'])) { + $item['content']['encoded'] = $item['atom_content']; + } + $item['md5'] = $this->generateMD5($item); + + $feedInDB = $this->getEntry($guid); + if (!$feedInDB) { + // check for category stuff + // we only do that for new entries + if (isset($item['dc']['subject'])) { + $item['category'] = $item['dc']['subject']; + } + + if ($row['cats']) { + $cats = explode(",",$row['cats']); + $hit = false; + foreach ($cats as $cat) { + if (strpos($item['category'],$cat) !== false) { + $hit = true; + } + } + + if (!$hit) { + print $item['title'] . " - " . $item['category'] . " not in list\n"; + continue; + } + } + // insert it in the db + $item = $this->truncateEntries($item); + $this->insertEntry($item, $row['id'], array("newBlog"=>$newBlog)); + } else if ($item['md5'] != $feedInDB['md5']) { + $item = $this->truncateEntries($item); + $this->updateEntry($item,$feedInDB['id']); + } + + } + } + } + + function truncateEntries($item) { + $maxsize = 5000; + if (isset($item['content']['encoded']) && strlen($item['content']['encoded']) > $maxsize + 500) { + print "TRUNCATE content_encoded on ". $item['title'] ."\n"; + $morebytes = (strlen($item['content']['encoded']) - $maxsize); + $item['content']['encoded'] = $this->getBody(substr($item['content']['encoded'],0,$maxsize)); + $item['content']['encoded'] .= '

Truncated by Planet Horde, read more at the original (another ' . $morebytes .' bytes)

'; + } else if (isset($item['description']) && strlen($item['description']) > $maxsize + 500) { + print "TRUNCATE description ". $item['title'] ."\n"; + $morebytes = (strlen($item['description']) - $maxsize); + + $item['description'] = $this->getBody(substr($item['description'],0,$maxsize)); + $item['description'] .= '

Truncated by Planet Horde, read more at the original (another ' . $morebytes .' bytes)

'; + } + return $item; + } + + function getBody($html) { + + $d = new DomDocument(); + $html = ''.$html.''; + @$d->loadHTML($html); + $xp = new domxpath($d); + $res = $xp->query("/html/body/node()"); + $body = ""; + foreach ($res as $node) { + $body .= $d->saveXML($node); + } + return $body; + } + + function generateMD5($item) { + return md5($item['title'] .$item['link'] . (isset($item['description']) ? $item['description'] : '') . (isset($item['content']['encoded']) ? $item['content']['encoded'] : '')); + } + + function updateEntry($item, $entryID) { + $date = $this->getDcDate($item, 0,true); + $query = "update entries set " . + " link = '" .mysql_escape_string(utf2entities($item['link'])) . "'," . + " title = '" .mysql_escape_string(utf2entities($item['title'])) . "'," . + " description= '" .mysql_escape_string(utf2entities($item['description'])) . "'," . + " content_encoded= '" . mysql_escape_string(utf2entities($item['content']['encoded'])) . "',"; + if ($date) { + $query .= " dc_date = '".$date."',"; + } + + $query .= " md5= '" .$item['md5'] . "' ". + " where ID = $entryID"; + print "update " . $item['title'] ."\n"; + $res = $this->mdb->query($query); + if (MDB2::isError($res)) { + print "DB ERROR: ". $res->getMessage() . "\n". $res->getUserInfo(). "\n"; + return false; + } else { + return true; + } + } + function insertEntry($item,$feedID, $options = array()) { + $id = $this->mdb->nextID("planet"); + if (isset($options['newBlog']) && $options['newBlog']) { + $offset = - 3600 * 144; // offset back to 6 days ago.needed for new blogs without pubdate/dcdate + } else { + $offset = 0; + } + if (!isset($item['guid']) || $item['guid'] == '') { + $item['guid'] = $item['link']; + } + + $date = $this->getDcDate($item, $offset); + + $query = "insert into entries (ID,feedsID, title,link, guid,description,dc_date, dc_creator, content_encoded, md5) VALUES (". $id . "," . + $feedID . ",'" . + + mysql_escape_string(utf2entities($item['title'])) . "','" . + mysql_escape_string(trim($item['link'])) . "','" . + mysql_escape_string(($item['guid'])) . "','" . + mysql_escape_string(utf2entities($item['description'])) . "','". + $date . "','" . + $item['dc']['creator'] . "','" . + mysql_escape_string(utf2entities($item['content']['encoded'])) . "','". + $item['md5'] . "')"; + + print "insert " . $item['title'] ."\n"; + $res = $this->mdb->query($query); + if (MDB2::isError($res)) { + print "DB ERROR: ". $res->getMessage() . "\n". $res->getUserInfo(). "\n"; + return false; + } else { + return $id; + } + } + + function getDcDate($item, $nowOffset = 0, $returnNull = false) { + //we want the dates in UTC... Looks like MySQL can't handle timezones... + //putenv("TZ=UTC"); + if (isset($item['dc']['date'])) { + $dcdate = $this->fixdate($item['dc']['date']); + } elseif (isset($item['pubdate'])) { + $dcdate = $this->fixdate($item['pubdate']); + } elseif (isset($item['issued'])) { + $dcdate = $this->fixdate($item['issued']); + } elseif (isset($item['created'])) { + $dcdate = $this->fixdate($item['created']); + } elseif (isset($item['modified'])) { + $dcdate = $this->fixdate($item['modified']); + } elseif ($returnNull) { + return NULL; + } else { + //TODO: Find a better alternative here + $dcdate = gmdate("Y-m-d H:i:s O",time() + $nowOffset); + } + return $dcdate; + + } + + function fixdate($date) { + $date = preg_replace("/([0-9])T([0-9])/","$1 $2",$date); + $date = preg_replace("/([\+\-][0-9]{2}):([0-9]{2})/","$1$2",$date); + $time = strtotime($date); + //if time is too much in the future (more than 1 hours) + // set it to now() + if (($time - time()) > 3600) { + $time = time(); + } + $date = gmdate("Y-m-d H:i:s O",$time); + return $date; + } + + function updateFeedBlogID($url, $id) { + + $query = "update feeds set blogsID = $id where link = '$url'"; + $res = $this->mdb->query($query); + if (MDB2::isError($res)) { + print "DB ERROR: ". $res->getMessage() . "\n". $res->getUserInfo(). "\n"; + return false; + } else { + return $id; + } + } + + function insertBlogEntry($channel) { + + $id = $this->mdb->nextID("planet"); + $query = "insert into blogs (ID,title,link,description) VALUES (". + $id . ",'" . + mysql_escape_string($channel['title']) . "','" . + mysql_escape_string($channel['link']) . "','" . + mysql_escape_string($channel['description']) . "')"; + $res = $this->mdb->query($query); + if (MDB2::isError($res)) { + print "DB ERROR: ". $res->getMessage() . "\n". $res->getUserInfo(). "\n"; + return false; + } else { + return $id; + } + } + + + function updateBlogEntry($channel,$id) { + + + $query = "update blogs set + title = '".mysql_escape_string($channel['title']) . "', + link = '".mysql_escape_string($channel['link']) . "', + description = '".mysql_escape_string($channel['description']) . "' where ID = ". $id; + + $res = $this->mdb->query($query); + if (MDB2::isError($res)) { + print "DB ERROR: ". $res->getMessage() . "\n". $res->getUserInfo(). "\n"; + return false; + } else { + return $id; + } + } + + function getBlogEntry($url) { + return $this->mdb->queryRow ("select * from blogs where link = '$url'",null,MDB2_FETCHMODE_ASSOC); + } + + function getFeedEntry($url) { + return $this->mdb->queryRow ("select * from feeds where link = '$url'",null,MDB2_FETCHMODE_ASSOC); + } + function getEntry($url) { + return $this->mdb->queryRow ("select * from entries where guid = '$url'",null,MDB2_FETCHMODE_ASSOC); + } + + function getRemoteFeed($url) { + if ($feed = fetch_rss($url)) { + return $feed; + } else { + print "$url is not a valid feed \n"; + return false; + } + } + +} diff --git a/hippo/planet.horde.org/libs/scripts/aggregate.php b/hippo/planet.horde.org/libs/scripts/aggregate.php new file mode 100644 index 000000000..282cdfd00 --- /dev/null +++ b/hippo/planet.horde.org/libs/scripts/aggregate.php @@ -0,0 +1,6 @@ +aggregateAllBlogs(isset($argv[1]) ? $argv[1] : null); diff --git a/hippo/planet.horde.org/libs/utf2entities.php b/hippo/planet.horde.org/libs/utf2entities.php new file mode 100644 index 000000000..fedcfdb56 --- /dev/null +++ b/hippo/planet.horde.org/libs/utf2entities.php @@ -0,0 +1,86 @@ +> 4; + + if ($asciiPos < 128) { + $pos += 1; + $thisLen = 1; + } + else if ($asciiRep == 12 or $asciiRep == 13) { + // 2 chars representing one unicode character + $thisLetter = substr ($source, $pos, 2); + $pos += 2; + $thisLen = 2; + } + else if ($asciiRep == 15) { + // 4 chars representing one unicode character + $thisLetter = substr ($source, $pos, 4); + $thisLen = 4; + $pos += 4; + } + else if ($asciiRep == 14) { + // 3 chars representing one unicode character + $thisLetter = substr ($source, $pos, 3); + $thisLen = 3; + $pos += 3; + } + + // process the string representing the letter to a unicode entity + + if ($thisLen == 1) { + $encodedLetter =$thisLetter; + } else { + $thisPos = 0; + $decimalCode = 0; + while ($thisPos < $thisLen) { + $thisCharOrd = ord (substr ($thisLetter, $thisPos, 1)); + if ($thisPos == 0) { + $charNum = intval ($thisCharOrd - $decrement[$thisLen]); + $decimalCode += ($charNum << $shift[$thisLen][$thisPos]); + } + else { + $charNum = intval ($thisCharOrd - 128); + $decimalCode += ($charNum << $shift[$thisLen][$thisPos]); + } + + $thisPos++; + } + if ($decimalCode < 65529) { + $encodedLetter = "&#". $decimalCode. ';'; + } else { + $encodedLetter = ""; + } + } + $encodedString .= $encodedLetter; + + } + return $encodedString; + } + diff --git a/hippo/planet.horde.org/themes/planet-horde/common.xsl b/hippo/planet.horde.org/themes/planet-horde/common.xsl new file mode 100644 index 000000000..a4a919bfc --- /dev/null +++ b/hippo/planet.horde.org/themes/planet-horde/common.xsl @@ -0,0 +1,34 @@ + + + + + + + + + + Planet Horde + + + + + + + + + + + + + + + + diff --git a/hippo/planet.horde.org/themes/planet-horde/css/screen.css b/hippo/planet.horde.org/themes/planet-horde/css/screen.css new file mode 100644 index 000000000..4186b0ac1 --- /dev/null +++ b/hippo/planet.horde.org/themes/planet-horde/css/screen.css @@ -0,0 +1,318 @@ +/* =ORDER + 1. display + 2. float and position + 3. width and height + 4. Specific element properties + 5. margin + 6. border + 7. padding + 8. background + 9. color +10. font related properties +----------------------------------------------- */ + +/* =MAIN +----------------------------------------------- */ +body, td, input, select, textarea, a { + font: 100%/1.45 'lucida grande', verdana, arial, sans-serif; +} +body { + margin: 0; + padding: 0; + background-color: #1B1B1B; + color: #333; +} +img, table { + border-width: 0; +} +table { + padding: 0; +} +td { + vertical-align: top; + margin: 0; + padding: 0; +} +a { + text-decoration: underline; +} +a:link, a:visited { + color: #4e5043; +} +a:hover { + color: #fff; + background-color: #3e4037; +} +h1 { + margin: 0 25px; + color: #4e5043; +} +h2 { + margin-top: 15px; +} +#main h4 { + margin: 7px 0 3px 0; +} + +/* =COMMON +----------------------------------------------- */ +/* clear float */ +.clr:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} +.clr {display: inline-block;} +/* Hides from IE-mac \*/ +* html .clr {height: 1%;} +.clr {display: block;} +/* End hide from IE-mac */ + +.none { + display: none; +} +.vtop { + vertical-align: top; +} +.vmid { + vertical-align: middle; +} +.vbot { + vertical-align: bottom; +} +.left { + float: left; +} +.right { + float: right; + text-align: right; +} +.center { + text-align: center; +} +.top { + margin-top: 5px; +} +code, .path { + background-color: #efefef; + font-family: "Courier New", Courier, monospace; + font-size: 90%; +} + +/* =ERROR PAGES 404/500 +----------------------------------------------- */ +#error { + width: 318px; + margin: 70px auto; + padding: 5px 30px 15px 30px; + background-color: #f9f9f9; + border: 1px solid #14507F; +} +#error #logo { + margin-bottom: 5px; +} +#error p { + margin: 10px 5px 0 5px; +} + +/* =LAYOUT +----------------------------------------------- */ +#wrapper { + width: 790px; + margin: 0 auto; +} +#header, #footer, #nav, #sub_nav, #content { + width: 790px; +} +#main { + float: left; + width: 470px; + margin: 0 25px; + display: inline; +} +#sidebar { + float: right; + width: 239px; + margin-right: 25px; + display: inline; +} + +/* =HEADER +----------------------------------------------- */ +#header { + padding: 5px 0; +} +#logo { + width: 300px; +} +#logo span, #logo a { + display: block; + width: 400px; + height: 88px; + background: url("../img/logo.gif") no-repeat; +} +#logo img { + display: block; + width: 0; +} + +#nav { + padding: 65px 0 0 0; + margin: 0; + width: 300px; + list-style: none; +} +#nav li { + float: left; + padding: 0 10px; + color: #fff; + font-size: 90%; +} +#nav li a { + color: #969696; +} +#nav li a:hover { + color: #fff; +} + +/* =SIDEBAR +----------------------------------------------- */ +.side_item { + background-color: #e1e2da; + margin-bottom: 15px; +} +.side_item h4 { + margin: 0; + padding: 4px 0 5px 0; + text-align: center; + background: url("../img/sidebar_head.gif") left top no-repeat; + font-size: 90%; +} +.side_item p, +.side_item dl, +.side_item form { + padding: 2px 12px 4px 12px; + margin: 0; + border-color: #4e5043; + border-width: 0 2px; + border-style: solid; + font-size: 85%; +} +.side_item dd { + margin: 0 0 0 10px; + padding: 0 0 5px 0; +} +.side_item .side_bottom { + font-size: 5px; + background: #f3f3f3 url("../img/sidebar_bottom.gif") 0 0 no-repeat; +} + +/* =CONTENT +----------------------------------------------- */ +#content { + padding: 10px 0 25px 0; + background: #f3f3f3 url("../img/content_corners.gif") top no-repeat; +} + +/* =FOOTER +----------------------------------------------- */ +#footer { + width: 740px; + background-color: #f8f8f8; + padding: 15px 25px 10px 25px; + border-top: 1px solid #c0c0c0; + border-bottom: 1px solid #c0c0c0; + font-size: 80%; +} + +/* =SYNTAX +----------------------------------------------- */ +.syntax { + padding: 5px; + background-color: #f8f8f8; + border: 1px solid #c0c0c0; + margin: 10px 0; + width: 460px; + overflow: auto; + line-height: 100%; +} +.syntax pre { + font-size: 90%; + margin: 0; + padding: 0; + background: transparent; + font-family: monospace; +} + +/* ruby */ +.syntax .normal { +} +.syntax .comment { + color: #808080; + font-style: italic; +} +.syntax .keyword { + color: #008800; + font-weight: bold; +} +.syntax .method { + color: #0066BB; + font-weight: bold; +} +.syntax .class { + color: #C40066; +} +.syntax .module { + color: #C40066; +} +.syntax .punct { + color: #333; +} +.syntax .symbol { + color: #AA6600; +} +.syntax .string { + color: #E6374B; + background-color: #FFF0F0; +} +.syntax .char { + color: #FFFBC7; +} +.syntax .ident { + color: #333; +} +.syntax .constant { + color: #003366; + font-weight: bold; +} +.syntax .regex { + color: #98AEC2; +} +.syntax .number { + color: #0000DF; +} +.syntax .attribute { + color: #3333BB; +} +.syntax .global { + color: #FFCE90; +} +.syntax .expr { + color: #333; +} +.syntax .escape { + color: #333; +} +.syntax .access { + color: #AA6600; +} + +/* yaml */ +.syntax .key { + color: #0066BB; +} +.syntax_yaml .punct { + color: #666; +} \ No newline at end of file diff --git a/hippo/planet.horde.org/themes/planet-horde/css/style.css b/hippo/planet.horde.org/themes/planet-horde/css/style.css new file mode 100644 index 000000000..650056fe8 --- /dev/null +++ b/hippo/planet.horde.org/themes/planet-horde/css/style.css @@ -0,0 +1,37 @@ +#box, .box { + margin: 0 0 2em 0; + padding: 0; +} + +.feedcontent { +margin: 1.12em 0; +/*overflow: auto; +max-height: 500px;*/ +} + +#pageNav { + padding: 0; + margin: 1em; +} + +.box h3 { + background-color: #e1e2da; + margin: 0 0 2px 0; + padding: 2px; +} +.box h3 a { + font-weight: bold; +} + +.blogLinkPad { + display: block; + text-decoration: none; + background: url("../img/feed-icon-10x10.png") no-repeat center left; + padding: 2px 0 2px 12px; + text-indent: 0; +} + +img { + max-width: 400px; + border: none; +} diff --git a/hippo/planet.horde.org/themes/planet-horde/img/content_corners.gif b/hippo/planet.horde.org/themes/planet-horde/img/content_corners.gif new file mode 100644 index 0000000000000000000000000000000000000000..e64f16ed7cb92d83dbc9da40ea91e0b9dfbfc748 GIT binary patch literal 132 zcmZ?wbhEHb6k}##Sj4~}EiLWl=C*0mruO#s&!0atz<}aU7BEu>M1s^ZFne+cIQ=s? z<+*yV#p|=X|0ihlrDV>_ShX(a^}Yg~^L+vu2OQqly#Ie-L4k*kRR4)3nHvJE=3iJ* a;9b#`aw-kX_Si^Jc1|2c;v&L+N%#GTkbr^vx_L@0?2ue1vae8uy9 zW>j2Fwi~;wnv#w|%)>D{wT>10d6+ znvjX&#K$e}$~4lwrKocpr#p$RZpK^viI-7mZAGBOZfK!ocn0%!gWCL!dO5}Fox+@M z<8LitMCck7=WdtW+z{sJ1iSwiD)WlBvkT!CKn3HAn`5Jatl8=pf zWMv()t_|gz0w^mJ$i`l18o*uui>ycxWHsvP8s|%U9u%*Cx=p;;psT*)T^`{*rxCTS zWX}(wG=ZN^W3ms(Xv}CQKedl?b7Aoq{>2_>AN82ZL&^z8{|hhxfn}MuvYci literal 0 HcmV?d00001 diff --git a/hippo/planet.horde.org/themes/planet-horde/img/logo.gif b/hippo/planet.horde.org/themes/planet-horde/img/logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..e6b9b2d7546044840dad255224ad6af3e46e87f0 GIT binary patch literal 5503 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 Z8iVRW>27Hm6QN|5P_bDULjVEW{{x&lR09A2 literal 0 HcmV?d00001 diff --git a/hippo/planet.horde.org/themes/planet-horde/img/planet-horde.psd b/hippo/planet.horde.org/themes/planet-horde/img/planet-horde.psd new file mode 100644 index 0000000000000000000000000000000000000000..911e50b90c090ab722abf6b5806b14ef8d8113c0 GIT binary patch literal 359925 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 literal 0 HcmV?d00001 diff --git a/hippo/planet.horde.org/themes/planet-horde/img/sidebar_bottom.gif b/hippo/planet.horde.org/themes/planet-horde/img/sidebar_bottom.gif new file mode 100644 index 0000000000000000000000000000000000000000..9368ad78f0929054826e7c7c34f97de3d097537b GIT binary patch literal 1670 zcmex=!ybuCbyfklv2NYT)dO*k--U8zvSsBz*# z4rQl}2StM}eo!$^Dr(~75)+q@lu}hw*U;25F*P%{u(Wb^admU|@bn4}2@MO6h>S{3 zNli=7$jmA(DJ?6nsH|#kX>Duo=>z(JGL-`{vmgtrq9L1*V<3BCp|FxsBZr97#DyCVaw;1KeGpA5 zy2vG_V)9V+BgkuDpAqM=CbE16_ZY%ow-|Vs8G(_97Q~P+n z#Xc#%tA2BSr+wOd!Pnt(S|8#M&%f1|-(CEAKG#;=s8`Q&lMmmMxKhKs^p*ImzShRH zk8!W|Dc`O(ocF1o@7C+fuj`aP>MyQ8wDUhh)0zJa=db?}mj3sj;onvJKh1IWR}2kz zwY=D&_A_SAm7P_w8e8jUUtcY>E9(8JW2G*JX8RoD5 zp=SQ~Kf~Wu^*`DAS0?>un9cwF%72C@4E7IW{xcj*tAGCbA7}4>hWh^uS@ZunF8(Ka z{*PGtzq?C%5JG=Tw^+1Oq3YnuMg4gVQx{r@>IV>KFw zGhRjPe|GrqSNop~qh1`ov6y}CUHyYu^S4}ooBh$=;d=dp#rFmO@n+p;vte8ock-cp zWBg{mg&(cmKRoZKiCuj&?&WnmjgNUdAKjK^Kk_ZS%7s7GY}U11`}U-z&O3Ir#&mh? zk2Zc$ZpOGgttBs3)to#(@$AW?)?0Qinv{3T`o-64*D{w~>$_baw>Nz5`ep6!dPU#x OXe?mrV!%)QzX<>ym=NIr literal 0 HcmV?d00001 diff --git a/hippo/planet.horde.org/themes/planet-horde/img/sidebar_head.gif b/hippo/planet.horde.org/themes/planet-horde/img/sidebar_head.gif new file mode 100644 index 0000000000000000000000000000000000000000..4e4b8a483f6578828f9c05e7a4bdcfb65f539612 GIT binary patch literal 4274 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 Q`2#iV!DsgQBLuALpMY2cYybcN literal 0 HcmV?d00001 diff --git a/hippo/planet.horde.org/themes/planet-horde/main.xsl b/hippo/planet.horde.org/themes/planet-horde/main.xsl new file mode 100644 index 000000000..0c040c86b --- /dev/null +++ b/hippo/planet.horde.org/themes/planet-horde/main.xsl @@ -0,0 +1,170 @@ + + + + + + + + + + + + +
+ +
+ + +
+ +
+ + +
+ + + + + + + + + + + () + + + + + + + + + + + + + + + + + +
+ + + + 10 + + + + + + + +
+ +
+ + +
+

+ + + +

+ + By + + + + () + + + + + + + + + ( UTC) +
+ + + + + + + + +
+
+
+ +
diff --git a/hippo/planet.sql b/hippo/planet.sql new file mode 100644 index 000000000..a6cfd391c --- /dev/null +++ b/hippo/planet.sql @@ -0,0 +1,134 @@ +-- MySQL dump 10.11 +-- +-- Host: localhost Database: planet +-- ------------------------------------------------------ +-- Server version 5.0.45 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `blogs` +-- + +DROP TABLE IF EXISTS `blogs`; +CREATE TABLE `blogs` ( + `ID` int(11) NOT NULL auto_increment, + `link` varchar(255) NOT NULL default '', + `title` varchar(255) NOT NULL default '', + `description` tinytext, + `changed` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `author` varchar(100) NOT NULL default '', + `dontshowblogtitle` tinyint(4) NOT NULL default '1', + PRIMARY KEY (`ID`), + UNIQUE KEY `link` (`link`) +) ENGINE=MyISAM AUTO_INCREMENT=46 DEFAULT CHARSET=latin1; + +-- +-- Dumping data for table `blogs` +-- + +LOCK TABLES `blogs` WRITE; +/*!40000 ALTER TABLE `blogs` DISABLE KEYS */; +INSERT INTO `blogs` VALUES (2,'http://janschneider.de/','Jan Schneider','News from the Horde Project','2008-02-26 19:56:57','',1),(17,'http://hagenbu.ch/','Chuck Hagenbuch','Chuck\'s blog, links, news, ramblings','2008-02-26 19:56:30','',1),(28,'http://mikenaberezny.com','Mike Naberezny','','2008-02-27 04:18:55','',1),(31,'http://theupstairsroom.com/','Michael Rubinsky','News to display on theUpstairsRoom.com website.','2008-02-26 22:28:29','',1),(45,'http://log.onthebrink.de/','Gunnar Wrobel',NULL,'2008-02-27 16:47:03','',1); +/*!40000 ALTER TABLE `blogs` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `entries` +-- + +DROP TABLE IF EXISTS `entries`; +CREATE TABLE `entries` ( + `ID` int(11) NOT NULL default '0', + `feedsID` int(11) NOT NULL default '0', + `title` tinytext, + `link` tinytext NOT NULL, + `guid` varchar(255) NOT NULL, + `description` text, + `dc_date` datetime default '0000-00-00 00:00:00', + `dc_creator` varchar(100) default NULL, + `content_encoded` text, + `changed` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `md5` varchar(32) NOT NULL default '', + PRIMARY KEY (`ID`), + UNIQUE KEY `link` (`link`(250)), + KEY `rss_feed_ID` (`feedsID`), + FULLTEXT KEY `search` (`description`,`content_encoded`,`title`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1; + +-- +-- Dumping data for table `entries` +-- + +LOCK TABLES `entries` WRITE; +/*!40000 ALTER TABLE `entries` DISABLE KEYS */; +INSERT INTO `entries` VALUES (7,1,'Patch library in PHP?','http://janschneider.de/news/35/318','http://janschneider.de/news/35/318','Does anybody know a good patch library in PHP? Like a counterpart to Text_Diff.\nI know that it\'s trivial to apply a clean patch file with a few lines of code, but I\'m looking for a more complete solution, especially with fuzzy patching capabilities.','2007-12-16 13:57:31','','','2008-02-25 21:42:48','7dcd12a5e870370a00fb52f16781b683'),(8,1,'Webmail comparison of DIMP, Roundcube and Scalix','http://janschneider.de/news/35/317','http://janschneider.de/news/35/317','As I mentioned in an earlier blog post about the article that I wrote for Linux Magazin Technical Review, there is another article from Linux Magazin which compares the AJAX webmail solutions DIMP, Roundcube, and the webmail component of Scalix. Here\'s a short summary.','2007-12-04 14:06:59','','','2008-02-25 21:42:48','e447af5e4850343c67d1baa7d1183608'),(9,1,'\"The Michigan Daily\" about Horde','http://janschneider.de/news/35/313','http://janschneider.de/news/35/313','The Michigan Daily released an article today about the University of Michigan NOT switching their mail system over to Google. They use Horde and IMP and the moment and seem to be very happy with it. There are more reasons why organizations and companies should not move to Gmail or Hotmail.','2007-11-21 23:26:00','','','2008-02-25 21:42:48','bf06d113f608c8a49036e3216b9176ff'),(10,1,'Article in Linux Magazin','http://janschneider.de/news/35/312','http://janschneider.de/news/35/312','An article about Horde has been released in a groupware edition of the German \"Linux Magazine - Technical Review\".','2007-11-20 12:19:00','','','2008-02-25 21:42:48','e01c82956ef670c0e0add982226eb894'),(11,1,'Kolab web client finished','http://janschneider.de/news/35/309','http://janschneider.de/news/35/309','From Gunnar Wrobel through the Kolab mailing lists: Kolab support in Horde is finally considered feature complete!','2007-05-30 22:02:28','','','2008-02-25 21:42:48','dcb37a899fd0d6f0c6e97b646ebeff9a'),(12,1,'Introduction to Horde_Lens','http://janschneider.de/news/35/308','http://janschneider.de/news/35/308','Chuck has written an introduction to the new Horde_Lens library: \"Horde_Lens is a decorating iterator implementation that lets you look at every element of a list through the lens of a Flyweight decorator object.\"','2007-05-14 22:11:32','','','2008-02-25 21:42:48','d0efa0368971d3a6539b096223ca183a'),(13,1,'Shop is open','http://janschneider.de/news/35/307','http://janschneider.de/news/35/307','It was just started for personal purposes, to get some Horde T-Shirts printed, but I was asked to make it open to the public. So here it is, the merchandising shop for the Horde Project.','2007-05-10 09:16:50','','','2008-02-25 21:42:48','0df3e97e67c60ad700ba60093617f824'),(14,1,'Nice new icon sets for Horde','http://janschneider.de/news/35/306','http://janschneider.de/news/35/306','Daniel Dembach has created a really nice icon set for Horde based on the FamFamFam silk icons, and a Tango icon set has been uploaded to our ticket system.','2007-03-21 10:39:26','','','2008-02-25 21:42:48','1ffac117115d4e1069689e09afed5ed8'),(15,1,'Vortrag bei der LUG Ravensberg','http://janschneider.de/news/35/305','http://janschneider.de/news/35/305','[To the English readers: this is about a (German) talk at a local linux user group.]\n\nAm 22. März werde ich in Bielefeld einen Vortrag über Horde bei der Linux Usergroup Ravensberg halten.','2007-03-20 09:15:29','','','2008-02-25 21:42:48','4f5bf4fb26b54479d65982b626e1a127'),(16,1,'Preparations for Horde 3.2 release series started','http://janschneider.de/news/35/304','http://janschneider.de/news/35/304','The release of 13 Horde applications during the last few days was the starting signal for the preparations of the Horde 3.2 release cycle. This series will not only deliver exciting new features for the stable applications, but turn some applications that are still in development into a first stable version, e.g. DIMP, Wicked or Whups.','2007-03-19 16:07:38','','','2008-02-25 21:42:48','b668da8be8b3db90dc9d1f9c82397d2f'),(18,2,'Horde/Yaml 1.0.1 released with PHP object support','http://hagenbu.ch/blog/64','http://hagenbu.ch/blog/64','The latest Horde/Yaml package supports the same !php/object constructs that the PECL syck package does.','2008-02-16 20:56:44','','','2008-02-25 21:42:48','57e3ac513880aee421a5782d2a66efb8'),(19,2,'Horde/Routes gets a home on the web','http://hagenbu.ch/blog/62','http://hagenbu.ch/blog/62','Horde has a great request routing library, and now Horde/Routes has a great website, too.','2007-11-02 22:42:20','','','2008-02-25 21:42:48','4e60ca66d062fdca3556160d62cf0439'),(20,2,'New Features in Horde 3.2','http://hagenbu.ch/blog/58','http://hagenbu.ch/blog/58','Focusing on a few of the new features in Horde 3.2','2007-08-17 22:41:40','','','2008-02-25 21:42:48','c7367a3fc16e43fdbabe792123f7a89b'),(21,2,'Horde 3.2 alpha releases!','http://hagenbu.ch/blog/56','http://hagenbu.ch/blog/56','Current status on the Horde 3.2 releases','2007-08-03 17:34:19','','','2008-02-25 21:42:48','92bba890cbc676ac129ebb3ff047f12a'),(22,2,'ORM with Horde_Rdo','http://hagenbu.ch/blog/55','http://hagenbu.ch/blog/55','An intro to ORM with Horde\'s Rdo data mapping library.','2007-06-12 20:51:00','','','2008-02-25 21:42:48','a0844dd45e95b91707a5aded83885b22'),(23,2,'Horde Application Building Podcast','http://hagenbu.ch/blog/54','http://hagenbu.ch/blog/54','A Horde podcast, recorded from a Horde Framework application-building talk at Boston PHP','2007-06-05 21:00:00','','','2008-02-25 21:42:48','17d849fabaf36ddadd513e46dbd4d2aa'),(24,2,'Horde_Lens','http://hagenbu.ch/blog/48','http://hagenbu.ch/blog/48','Horde_Lens is a decorating iterator implementation that lets you look at every element of a list through the lens of a Flyweight decorator object.','2007-05-12 16:04:00','','','2008-02-25 21:42:48','94534540587e922e3ad13be66cd4bae9'),(25,2,'Plugs for Horde Groupware','http://hagenbu.ch/blog/49','http://hagenbu.ch/blog/49','Think Horde is too hard to install? Think again.','2007-04-28 01:55:00','','','2008-02-25 21:42:48','041d5630f814f4b14f774adaa51d7441'),(26,2,'NYPHPCon slides are up','http://hagenbu.ch/blog/44','http://marina.horde.org/horde/jonah/stories/view.php?channel_id=133&story_id=44','Also, I\'m not going to PHPVikinger.','2006-06-22 21:36:00','','','2008-02-25 21:42:48','793522b0d5dca7689e74bcaa7f9bb3f5'),(27,2,'Horde Tutorial at NYPHPCon','http://hagenbu.ch/blog/43','http://marina.horde.org/horde/jonah/stories/view.php?channel_id=133&story_id=43','Mashup web 2.0 mashup web 2.0 mashup web 2.mmppphhhhh!','2006-05-28 16:54:00','','','2008-02-25 21:42:48','ce2dfca573869aed82c3fc240bcdb495'),(29,3,'Horde/Yaml 1.0 Released','http://mikenaberezny.com/2008/01/08/hordeyaml-10-released/','http://mikenaberezny.com/archives/87','','2008-02-19 21:42:49','','

Horde/Yaml is a PHP 5 library for easily working with YAML data. This is the package’s first stable release.

\n

Chuck Hagenbuch started the library as an adaptation of Spyc around six months ago. Since then, he and I have been quietly using and improving it. Along the way, we fixed many issues, added support for pecl/syck, and wrote a test suite with PHPUnit.

\n

There are a couple of other libraries also derived from Spyc, notably the sfYaml class from the Symfony framework. Since these efforts also found and corrected issues, we incorporated as many of these fixes as we could find and added them to the test suite as we went along.

\n

At Maintainable Software, we frequently use YAML files for configuring our custom applications because our clients tend to like the format more than the alternatives. We’ve been using Horde/Yaml successfully for quite some time so we think it should generally work well for you also.

\n

There’s a nice tutorial on working with YAML in PHP 5 over on the new Rails for PHP Developers website. It includes everything you need to get started with Horde/Yaml.

\n','2008-02-25 21:42:49','10168ad80892ea3bbcc08520bcd98460'),(30,3,'New in Horde: Routes','http://mikenaberezny.com/2007/09/15/new-in-horde-routes/','http://mikenaberezny.com/archives/80','','2008-02-19 21:42:49','','

I’m pleased to announce the first release of Horde/Routes, a new URL mapping system for PHP 5. This package provides classes for mapping URLs into the controllers and actions of an MVC system, inspired by Ruby on Rails.

\n

There are already quite a few existing libraries that do this sort of thing for PHP. Horde/Routes is a compelling alternative.

\n

At Maintainable, we examined most of these PHP solutions and found them all inadequate for various reasons, particularly because we wanted RESTful routing, named routes, sophisticated matching, PHP 5 E_STRICT, and extensive test coverage.

\n

Since we do quite a bit of Ruby and Python programming, we surveyed the options in those languages and decided to do a full port of the Python library, Routes.

\n

Horde/Routes provides these features and more:

\n
    \n
  • Supports route recognition and generation
  • \n
  • Sophisticated matching conditions like subdomains
  • \n
  • Named routes and RESTful route generation
  • \n
  • PEAR-style naming and coding standards
  • \n
  • PHP 5 E_STRICT compliant, web framework agnostic
  • \n
  • A comprehensive unit test suite
  • \n
\n

Maintainable decided to contribute the code to Horde’s Rampage project because Horde is one of PHP’s oldest and most successful projects. We’re using and contributing to other Horde libraries and we think Rampage is worth your attention.

\n

The Python version has been around for some time and is very popular with different Python web frameworks. We’re happy to be part of that ecosystem now and our full port has already resulted in patches being committed back to the Python version.

\n

At Maintainable, we’ve already used Horde_Routes on several applications. While Horde/Routes is relatively new, it is very feature-rich and well-tested.

\n

Currently, Horde/Routes is a beta release. Over the coming weeks, we’ll be making some minor changes to the API and adding more documentation, and then it will quickly move to stable.

\n

Update: The project now has its own pages on the Horde website and was featured on Chuck Hagenbuch’s blog.

\n','2008-02-25 21:42:49','3cce684da4477be8c1c54010bd013df8'),(32,4,'Lighttpd configuration specific to Horde','http://theupstairsroom.com/61','http://theupstairsroom.com?story=61','Some example entries for lighttpd.conf file for duplicating various .htaccess rules in various Horde applications','2008-02-03 21:24:00','','','2008-03-29 16:00:12','f2b58275002a6277b03c204af0dbe5bd'),(33,4,'Ansel gets RSS support','http://theupstairsroom.com/58','http://rubinskyfamily.com/mike/58/','Ansel, the Horde Project\'s photo application gets support for RSS feeds.','2007-10-05 19:21:00','','','2008-03-29 16:00:12','ab9b22ea4675eec2c72550076b25b0db'),(34,4,'Aeros, the aviation based Horde theme has been updated','http://theupstairsroom.com/50','http://theupstairsroom.com/news/view.php?feed=124&story=50','I posted a new tarball for my aviation inspired Horde theme, Aeros. It\'s a small update that includes a few tweaks and some CSS cleanup.','2007-06-21 14:27:00','','','2008-03-29 16:00:12','be53eba64112bcc979b045b3ef9555b5'),(35,4,'Some new work on Ansel','http://theupstairsroom.com/47','http://rubinskyfamily.com/news/138/47/','I\'ve been scratching some coding itches again with the Horde Project, adding some new features to Ansel, the image gallery application.','2007-05-09 17:28:00','','','2008-03-29 16:00:12','fa923b5fae42c71c6b34cd1d33e1ec73'),(36,4,'phpSysInfo Block updated','http://theupstairsroom.com/16','http://theupstairsroom.com/news/view.php?feed=124&story=16','Some minor updates and bugfixes for the phpSysInfo block have been released...','2006-07-30 20:18:00','','','2008-03-29 16:00:12','2a00a2d1858e255a43312a4772bc3cc9'),(37,4,'New Horde Block - phpSysInfo','http://theupstairsroom.com/7','http://theupstairsroom.com/news.php?channel_id=124&story_id=7','Displays stats from machines that are running phpSysInfo on the Horde portal page.','2006-03-16 20:59:00','','','2008-03-29 16:00:12','1f6f974d04a43425367558704b01f027'),(43,2,'Planet Horde Launches','http://hagenbu.ch/blog/65','http://hagenbu.ch/blog/65','We\'ve just launched Planet Horde: http://planet.horde.org/. It\'s more of a small moon right now, but it\'s the place to be for Horde news and articles.','2008-02-27 03:39:38','','','2008-02-27 04:16:13','51086e0c8176a5b1324ac891e19d08a8'),(45,5,'Horde: Synching with HEAD','http://log.onthebrink.de/2008/02/horde-synching-with-head.html','tag:blogger.com,1999:blog-998418780028044861.post-3593761677129855906','','2008-02-27 16:48:37','','

\nFeels good to be finally back working on Horde. Last week saw the generation of Kolab patches for Horde-3.2-RC2 and this week I finally synchronized my work environment with Horde CVS. The first Kolab commit to Horde CVS went in yesterday. It felt like the last one was ages ago.\n

\n

\nAnyhow there are more commits ahead. Now that most parts of Horde work with Kolab the time for the second round of coding is approaching fast: restructuring and optimization. There is still a lot that needs to be done and I\'m desperately waiting for Horde 4 to finally restructure the whole Kolab module and get it on a hopefully sane path for the future.\n

','2008-02-27 16:48:37','8b1cbe6b9bc952260d3681ee5f26e5bb'),(46,5,'K2G: Kolab2/Gentoo getting attention again','http://log.onthebrink.de/2007/12/finally-kolab2gentoo-project-gets-some.html','tag:blogger.com,1999:blog-998418780028044861.post-818745145701986043','','2008-02-27 16:48:37','','Finally the Kolab2Gentoo project gets some attention again. I started fixing some older bugs but the main part will be to prepare for 2.2 now.\n\nI still hate the way the whole project is structured but there are too many places where massive improvements are needed. So I guess I have to be content with small progress.\n\nThe stuff that is currently on my mind concerning Kolab2/Gentoo-2.2:\n\n
    \n
  • Update to Horde-3.2
  • \n
  • Making the patched c-client and php packages recommended instead of required
  • \n
  • Switching to a specific cyrus-imapd-kolab package
  • \n
  • Template packages
  • \n
\n\nAnd then there is the big, bad problem of the Kolab configuration concept that simply fails for Gentoo in its current form. The solution to that one will take even longer.','2008-02-27 16:48:37','94fe41b14e894979c8d96411e60bcdb0'),(47,2,'Creating a Site Specific Browser for Horde','http://hagenbu.ch/blog/66','http://hagenbu.ch/blog/66','Fluid lets you create a completely encapsulated, WebKit-based browser for your web application. Here\'s a guide to making one for Horde.','2008-03-03 04:16:08','','','2008-03-03 05:00:18','0da4d9d5315f6a9ffc42531022ae84fa'),(48,4,'Update on Ansel development','http://theupstairsroom.com/63','http://theupstairsroom.com?story=63','With Horde 3.2 quickly approaching final release status, I\'ve been working on getting Ansel ready for an upcoming release. Many new features and performance improvements are on the way.','2008-03-05 00:41:01','','','2008-03-29 16:00:12','c00e4fc2e57aac033969f90894c95f4b'),(49,1,'Nice helper to create RTL CSS files','http://janschneider.de/news/35/319','http://janschneider.de/news/35/319','Via Ajaxian: CSSJanus is a CSS parser utility designed to aid the conversion of a website\'s layout from left-to-right (LTR) to right-to-left (RTL).','2008-03-05 17:28:00','','','2008-03-15 17:00:14','ac6d5e0ece6a923420e0054668d36969'),(50,2,'March Horde Board Meeting Summary','http://hagenbu.ch/blog/67','http://hagenbu.ch/blog/67','The second Horde advisory board meeting discussed Horde 3.2, Google Summer of Code, and Planet Horde','2008-03-08 02:51:29','','','2008-03-08 03:00:11','69dd115c22761eb892f3b950edf04899'),(51,5,'Sync my Kolab','http://log.onthebrink.de/2008/03/sync-my-kolab.html','tag:blogger.com,1999:blog-998418780028044861.post-2300864698879546636','','2008-03-12 21:00:14','','\"\"\n

\nSyncML support for the Kolab server has been requested for several years now. Supporting it via the modules available within Horde always seemed to be one the of easiest ways to get mobile clients to synchronize with the server. Since the newest Kolab server release candidates now provide Horde, how far is SyncML support away?\n

\n

\nNot far at all... Univention contracted p@rdus via the Kolab Konsortium to implement SyncML support within the Kolab server.\n

\n

\nInitially a version that would require an additional MySQL database was planned but p@rdus invested some additional time into generating purely IMAP based drivers so that SyncML support will also be available within the next Kolab release (the Kolab server does not use MySQL at all by default).\n

\n

\nToday I was able to sync the Blackberry provided by the customer for the first time. Contacts, events, tasks all survived my minimal testing. Of course the same procedure failed once I gave the customer access to the test server...\n

\n

\nSo right now I\'m entering the debugging phase and I\'m starting to prepare some scripts so that people eager to try the SyncML support can install an experimental Horde version on an external web server.\n

\n

Update:

\n

\nA script for installing horde from CVS is now available. It also installs all the required Kolab patches for SyncML support.\n

\n

\nYou can fetch and run it like this:\n

\nwget http://kolab.org/cgi-bin/viewcvs-kolab.cgi/*checkout*/server/horde/external-horde-cvs.sh\nchmod u+x external-horde-cvs.sh\n./external-horde-cvs.sh\n
\n

','2008-03-13 08:00:15','d7a46ef783b154fcf305796488831209'),(52,2,'Horde 3.2: Serving multiple subdomains with one installation','http://hagenbu.ch/blog/68','http://hagenbu.ch/blog/68','Horde 3.2 includes improved support for spreading Horde apps across multiple subdomains. Want to know how to set up a single Horde install to serve, for example, bugs.horde.org, wiki.horde.org, cvs.horde.org, and dev.horde.org?','2008-03-19 03:35:42','','','2008-03-19 04:00:22','1b978091a8efba37d2ff2ece04b16e07'),(53,4,'Using the Horde API to Power External Sites or Applications - Part 1','http://theupstairsroom.com/65','http://theupstairsroom.com?story=65','Horde and all of it\'s applications have a powerful API through which a developer can obtain Horde content. In this first part of a series, we\'ll look at the basics of what is required to interact with Horde via the API.','2008-03-29 15:12:00','','','2008-03-29 16:00:12','a5ac7bade123afc291b118e530487a37'),(54,2,'Auditing Horde applications with taint mode','http://hagenbu.ch/blog/69','http://hagenbu.ch/blog/69','Using Wietse Venema\'s taint mode modification to PHP to audit Horde','2008-04-04 20:58:39','','','2008-04-04 21:00:14','94248a828ee7fa67805baca26245d41e'),(55,5,'Another round of Horde bugs...','http://log.onthebrink.de/2008/04/another-round-of-horde-bugs.html','tag:blogger.com,1999:blog-998418780028044861.post-2482205407364385305','','2008-04-24 15:00:39','','I\'m back to Horde bug fixes and while their CVS server vanished in some kind of limbo I took the time to create a Horde/Kolab project page. Maybe it is a useful overview to the people interested in Horde. I definitely have to update the Kolab wiki, too. But that might still take a while.','2008-04-24 15:00:39','d261c6b2137d94725520255f71386ef1'),(56,5,'The OpenSourceSchool opens its doors','http://log.onthebrink.de/2008/04/opensourceschool-opens-its-doors.html','tag:blogger.com,1999:blog-998418780028044861.post-1758429239832563689','','2008-04-25 09:00:25','','\"\"\n

\nMy publisher started with his next endeavor in bringing knowledge to the masses: The OpenSourceSchool. This time it is about spoken words - or courses - rather than written pages bound as books. Many OpenSourcePress authors are offering seminars there.\n

\n

\nI would definitely have liked to offer a course about Gentoo there. But I had to agree with them that this would probably not raise enough interest from paying customers. Or am I wrong about that?\n

\n

\nBut of course there was room for the second topic dear to my heart: Kolab. The course will take five days and touch all major topics of the Kolab Server. Central components such as postfix, openldap, cyrus imap will provide the core components but I\'ll certainly also include a chapter about getting the Horde web client successfully installed. So we will hopefully have a new batch of Kolab experts in October.\n

\n

\nAnd hopefully the preparations for the course will also help in laying the groundwork for a book about Kolab. This is the only book I still want to write after going through the pain of writing the Gentoo book.\n

','2008-04-25 10:00:39','7d941c49ba3832a392f885ea09e901cf'),(57,2,'Preventing Email Abuse through IMP','http://hagenbu.ch/blog/70','http://hagenbu.ch/blog/70','Some IMP installations have been targeted by brute-force or phishing attacks; compromised accounts are then used to send spam. Horde 3.2 and IMP 4.2 have some new features for limiting the damage of a compromised account or a rogue user.','2008-04-30 03:05:17','','','2008-04-30 04:00:17','39aec04ae0868557871d2499734dc41b'),(58,2,'May Horde Board Meeting Summary','http://hagenbu.ch/blog/71','http://hagenbu.ch/blog/71','Discussed: Horde 3.2 releases (this month), marketing material, showstoppers, and the new www.horde.org project.','2008-05-07 03:42:30','','','2008-05-07 04:00:33','2ae3e29192a1d1dacf6333eed74ebeb7'),(59,4,'Using the Horde API to Power External Sites or Applications - Part 2','http://theupstairsroom.com/66','http://theupstairsroom.com/66','In this second part of the series, we will look at using Horde\'s RPC server to get information from a remote Horde server via Horde\'s RPC interface.','2008-05-07 18:26:58','','','2008-05-07 19:00:22','43b3f6c56496b3c249e6a254a9e54486'),(60,4,'Using the Horde API to Power External Sites or Applications - Part 3','http://theupstairsroom.com/67','http://theupstairsroom.com/67','In this installment, we\'ll take a look at a real world example of using Horde to power an external website by building a custom photo gallery with Ansel.','2008-05-08 04:21:44','','','2008-05-08 05:00:33','ef109d9ee2d2b5901fdeca7222cfed69'),(61,2,'Birth Announcement','http://hagenbu.ch/blog/72','http://hagenbu.ch/blog/72','I\'m a dad! Some details and a few thumbnails after the jump.','2008-05-12 21:15:03','','','2008-05-12 22:00:28','c0b1fd939aeca7ddafd933f7b9459578'); +/*!40000 ALTER TABLE `entries` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `feeds` +-- + +DROP TABLE IF EXISTS `feeds`; +CREATE TABLE `feeds` ( + `ID` int(11) NOT NULL auto_increment, + `blogsID` int(11) NOT NULL default '0', + `link` varchar(255) NOT NULL default '', + `changed` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `cats` varchar(255) NOT NULL default '', + `section` varchar(50) NOT NULL default 'default', + PRIMARY KEY (`ID`), + UNIQUE KEY `rssURL` (`link`), + KEY `blogID` (`blogsID`) +) ENGINE=MyISAM AUTO_INCREMENT=6 DEFAULT CHARSET=latin1; + +-- +-- Dumping data for table `feeds` +-- + +LOCK TABLES `feeds` WRITE; +/*!40000 ALTER TABLE `feeds` DISABLE KEYS */; +INSERT INTO `feeds` VALUES (1,2,'http://janschneider.de/horde/jonah/delivery/rss.php?channel_id=35','2008-02-27 04:19:11','','default'),(2,17,'http://technest.org/horde/jonah/delivery/rss.php?channel_id=133&tag_id=4','2008-02-27 04:19:24','','default'),(3,28,'http://mikenaberezny.com/wp-atom.php?tag=horde','2008-02-25 21:42:49','','default'),(4,31,'http://portal.theupstairsroom.com/horde/jonah/delivery/rss.php?channel_id=124&tag_id=10','2008-02-27 04:19:37','','default'),(5,45,'http://log.onthebrink.de/feeds/posts/default/-/horde','2008-02-27 16:47:53','','default'); +/*!40000 ALTER TABLE `feeds` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `planet_seq` +-- + +DROP TABLE IF EXISTS `planet_seq`; +CREATE TABLE `planet_seq` ( + `sequence` int(11) NOT NULL auto_increment, + PRIMARY KEY (`sequence`) +) ENGINE=MyISAM AUTO_INCREMENT=62 DEFAULT CHARSET=latin1; + +-- +-- Dumping data for table `planet_seq` +-- + +LOCK TABLES `planet_seq` WRITE; +/*!40000 ALTER TABLE `planet_seq` DISABLE KEYS */; +INSERT INTO `planet_seq` VALUES (61); +/*!40000 ALTER TABLE `planet_seq` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2008-05-13 19:18:38 diff --git a/hound/urls b/hound/urls new file mode 100644 index 000000000..1c13b1cc1 --- /dev/null +++ b/hound/urls @@ -0,0 +1,10 @@ +Great keyboard nav idea: +http://parand.com/say/index.php/2008/09/15/readerscroll-google-reader-style-image-navigation-with-j-k-keys-bookmarklet/ + +http://www.readwriteweb.com/archives/opera_96_launches_now_includes_magazine_style_rss.php +http://www.readwriteweb.com/archives/will_gmail_get_google_reader-like_trends.php +http://www.readwriteweb.com/archives/google_reader_now_lets_you_sha.php +http://www.readwriteweb.com/archives/feedly_launches_a_river_of_news.php +http://www.readwriteweb.com/archives/bloglines_is_still_alive_and_advertising.php +http://www.readwriteweb.com/archives/mainstream_web_watch_why_alltop_rocks.php +http://www.readwriteweb.com/archives/mainstreaming_rss_regator_public_beta.php diff --git a/hydra/lib/Page.php b/hydra/lib/Page.php new file mode 100644 index 000000000..7348bbc9a --- /dev/null +++ b/hydra/lib/Page.php @@ -0,0 +1,6 @@ + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^(.*)$ index.php [QSA,L] + diff --git a/hydra/public/index.php b/hydra/public/index.php new file mode 100644 index 000000000..a4abe2daf --- /dev/null +++ b/hydra/public/index.php @@ -0,0 +1,2 @@ +