catkin

filter_orage_ics_for_gnokii.awk 0.1

May 20th, 2011
266
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/usr/bin/awk -f
  2.  
  3. # Best viewed with tabs every 4 characters
  4.  
  5. #   Copyright (C) 2011 Charles
  6. #
  7. #   This program is free software; you can redistribute it and/or modify
  8. #   it under the terms of the GNU General Public License as published by
  9. #   the Free Software Foundation; either version 2 of the License, or
  10. #   (at your option) any later version.
  11. #
  12. #   This program is distributed in the hope that it will be useful,
  13. #   but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15. #   GNU General Public License for more details.
  16. #
  17. #   You should have received a copy of the GNU General Public License
  18. #   along with this program; if not, write to the Free Software
  19. #   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, US
  20.  
  21. #   Acknowledgements and thanks to Kjell-Inge Gustafsson of kigkonsult for
  22. #   some algorithms inspired by iCalcreator class v2.6 PHP code at
  23. #   http://svn.oscc.org.my/taskmanager/branches/2.0/app/vendors/iCalcreator.class.php
  24.  
  25. #   Acknowledgements and thanks to Concordia University for date calculation
  26. #   theory at http://alcor.concordia.ca/~gpkatch/gdate-method.html and sample
  27. #   awk code at http://alcor.concordia.ca/~gpkatch/gdate-awk.html
  28.  
  29. # Name: filter_orage_ics_for_gnokii.awk
  30.  
  31. # Purpose:
  32. #   Filters an Orage export .ics file in preparation for gnokii to write to a
  33. #   Nokia Series 40 mobile phone:
  34. #    * Converts recurring VEVENTs to a series of single VEVENTs.
  35. #    * Removes VEVENTs outside the time range -1 to +2 months.
  36.  
  37. # Usage: -h option for help (runs the usage function)
  38.  
  39. # Environment:
  40. #   Developed and tested on Slackware64 13.1 using
  41. #    * Xfce Orage 4.6.1
  42. #    * GNU Awk 3.1.8
  43. #    * gnokii from git snapshot 5may11
  44. #    * Nokia 5130c-2 (RM-495 with 07.97 firmware, USB 2.0 connection)
  45.  
  46. # Support: please contact Charles via the gnokii users mailing list:
  47. #     http://lists.nongnu.org/mailman/listinfo/gnokii-users
  48.  
  49. # History:
  50. #  12may11 Version 0.0. Charles
  51. #    * Limited functionality, proof of concept.
  52. #  17may11 Version 0.1. Charles
  53. #   * Command line:
  54. #     -d option to set new Debug_level
  55. #     -h option to print help using new usage function
  56. #     -V option to print version
  57. #   * Support for BYDAY as used by Orage (except YEARLY which is
  58. #     incomprehensible or broken)
  59. #   * Startup message including this script's version number
  60. #  23may11 Version 0.2. Charles
  61. #   * Changed tabs to spaces and applied max 80 line length
  62. #   * Changed RFC 2445 references to RFC 5545
  63. #   * Command line:
  64. #     -a option to make all alarms audio
  65. #     -v option to make all alarms visual
  66.  
  67. # TODO: in approx descending priority
  68. #   * Command line:
  69. #     - Logging level option - D, W, I, E
  70. #     - Log file name option
  71. #     - Output time range options
  72. #     - Input and output file options
  73. #   * Output VEVENTs with an alarm time in the output time range.
  74. #   * Use time zone information?
  75. #   * Service TODOs and JOURNALs as well as VEVENTs?
  76.  
  77. # Programmers' notes:
  78. #   * The input file format is a subset of "RFC 5545, Internet Calendaring and
  79. #     Scheduling Core Object Specification (iCalendar)" as implemented by the
  80. #     Xfce calendar application, Orage 4.6.1. The text of RFC 5545 is at
  81. #     http://tools.ietf.org/html/rfc5545
  82. #   * This script is also intended as a prototype for C code so it:
  83. #     - Does not use any awk pattern-actions; everything is done by the BEGIN
  84. #       action.
  85. #     - Lists function local variables on a separate line from arguments.
  86. #   * Variable name components:
  87. #     - date  DATE as defined in RFC 5545
  88. #     - doe   Day of epoch beginnning on 1 March of Gregorian calendar
  89. #             projected backwards to year 1.
  90. #     - time  TIME as defined in RFC 5545
  91. #   * Global variables (distinguished by an initial upper-case letter):
  92. #     - Audio_alarm  0 for no coercion, 1 for coercion
  93. #     - Byday_list  BYDAY list
  94. #     - Bymonth_list  BYMONTH list
  95. #     - Bymonthday_list  BYMONTHDAY list
  96. #     - Bysetpos_list  BYSETPOS list
  97. #     - Byyearday_list  BYYEARDAY list
  98. #     - Count  COUNT value
  99. #     - Debug_level  0 for no debugging. Usage message gives further detail.
  100. #     - Dtend  DTEND value
  101. #     - Dtend_date
  102. #     - Dtend_line  Line of VEVENT in which DTEND is given
  103. #     - Dtend_time
  104. #     - Dtstart  DTSTART value
  105. #     - Dtstart_date
  106. #     - Dtstart_line  Line of VEVENT in which DTSTART is given
  107. #     - Dtstart_time
  108. #     - Dtstart_time_in_days  DTSTART's time component as a float number of days
  109. #     - Duration  DURATION value
  110. #     - Duration_days  DURATION as a float number of days
  111. #     - Freq  FREQ value
  112. #     - Interval  INTERVAL value
  113. #     - Lines  The lines of a VEVENT
  114. #     - Month_day_counts  Array of 31,28,31,30,31,30,31,31,30,31,30,31
  115. #     - My_name  Last component of path by which this script was called
  116. #     - N_lines  The number of lines in Lines
  117. #     - Optarg  An option's argument
  118. #     - Opterr  Function get_opt error control; set to 0 to suppress error messages
  119. #     - Optind  Index of the first non-option argument in ARGV
  120. #     - Optopt  An option's letter
  121. #     - Output_date_range_end_date
  122. #     - Output_date_range_end_doe
  123. #     - Output_date_range_start_date
  124. #     - Output_date_range_start_doe
  125. #     - Recurrence  1 if the VEVENT has an RRULE otherwise 0
  126. #     - Until  UNTIL value
  127. #     - Until_date
  128. #     - Version  This script's version
  129. #     - Visual_alarm  0 for no coercion, 1 for coercion
  130. #     - Weekdays  Array of SU,MO,TU,WE,TH,FR,SA
  131. #     - Wkst  WKST value
  132. #   * Global variables for setter's use only (distinguished by an initial _):
  133. #     - _opti  Set by the getopt() function
  134. #   * Function call tree:
  135. #   BEGIN
  136. #   |
  137. #   +-- initialise
  138. #   |   |
  139. #   |   +-- getopt
  140. #   |   |
  141. #   |   +-- usage
  142. #   |
  143. #   +-- get_begin_vevent (normal exit)
  144. #   |
  145. #   +-- get_vevent
  146. #   |   |
  147. #   |   +-- parse_dtend
  148. #   |   |
  149. #   |   +-- parse_dtstart
  150. #   |   |
  151. #   |   +-- parse_duration
  152. #   |   |
  153. #   |   +-- parse_rrule
  154. #   |
  155. #   +-- do_vevent
  156. #   |   |
  157. #   |   +-- write_vevent
  158. #   |
  159. #   +-- do_vevents
  160. #       |
  161. #       +-- get_instance_start_dates
  162. #       |   |
  163. #       |   +-- get_instance_start_dates_for_month_byday
  164. #       |   |
  165. #       |   +-- get_instance_start_dates_for_week_byday
  166. #       |
  167. #       +-- modify_dtstart_date
  168. #       |
  169. #       +-- modify_dtend_date
  170. #       |
  171. #       +-- write_vevent
  172. #
  173. # Utility functions
  174. #    
  175. # convert_date_to_doe, convert_doe_to_date, convert_duration_to_days,
  176. # convert_time_to_days, date_math_add_months, date_math_add_weeks,
  177. # date_math_add_years, get_month_end_date, get_month_start_date,
  178. # get_week_start_doe, in_output_range_by_date, in_output_range_by_doe,
  179. # leap_year, msg, my_getline, parse dt_time, parse_dt, parse_dt_date
  180.  
  181. # Function definitions in alphabetical order.  Execution begins after the
  182. # last function definition (search for BEGIN).
  183.  
  184. function convert_date_to_doe ( date,  
  185.     y, m, d )
  186. {
  187.     y = substr( date, 1, 4 )
  188.     m = substr( date, 5, 2 )
  189.     d = substr( date, 7, 2 )
  190.     # Convert to calendar with March as first month
  191.     m = ( m + 9 ) % 12
  192.     y = y - int( m / 10 )
  193.     return y * 365 + int( y / 4 ) - int( y / 100 ) + int( y / 400 ) \
  194.         + int( 0.5 + m * 30.6 ) + d - 1
  195. }
  196.  
  197. function convert_doe_to_date ( doe,
  198.     d, day_of_year, m, m_intermediate, y, y_start_doe )
  199. {
  200.     # Calculate approximate year and its first day of epoch number
  201.     y = int( ( doe + 1.4780 ) / 365.2425 )
  202.     y_start_doe = convert_date_to_doe( y "0301" )
  203.  
  204.     # Calculate the day of year, adjusting the approximate year if necessary
  205.     if ( doe >= y_start_doe )
  206.     {
  207.         day_of_year = doe - y_start_doe
  208.     }
  209.     else
  210.     {
  211.         y -= 1
  212.         day_of_year = doe - convert_date_to_doe( y "0301" )
  213.     }
  214.  
  215.     # Calculate final year, month and day of month
  216.     m_intermediate = int( .017 + day_of_year / 30.6 )
  217.     m = ( m_intermediate + 2 ) % 12 + 1
  218.     y += int( ( m_intermediate + 2) / 12 )
  219.     d = day_of_year - int( m_intermediate * 30.6 + 0.5) + 1
  220.  
  221.     return sprintf("%d%02d%02d", y, m, d)
  222. }
  223.  
  224. function convert_doe_to_weekday ( doe,
  225.     d, date, first_of_month_doy, m, m_adjusted, my_return, y, y_adjusted, \
  226.     y_adjustment )
  227. {
  228.     # strftime cannot be used because it is locale-dependent
  229.     # The algorithm is Zeller's congruence
  230.    
  231.     date = convert_doe_to_date( doe )
  232.     y = substr( date, 1, 4 )
  233.     m = substr( date, 5, 2 )
  234.     d = substr( date, 7 )
  235.  
  236.     # Adjust the year and month to a 1st March based year
  237.     y_adjustment = int( ( 14 - m ) / 12 )
  238.     y_adjusted = y - y_adjustment
  239.     m_adjusted = m + 12 * y_adjustment - 2
  240.  
  241.     # Calculate the first day of the month as the day of a 1st March based year
  242.     first_of_month_doy = y_adjusted \
  243.         + int( y_adjusted / 4 ) - int( y_adjusted / 100 ) \
  244.         + int( y_adjusted / 400) + int( ( 31 * m_adjusted ) / 12 )
  245.  
  246.     my_return = Weekdays[ ( first_of_month_doy + d ) % 7 + 1 ]
  247.     msg( "D100", \
  248.         "convert_doe_to_weekday: date: " date ", my_return: " my_return )
  249.     return my_return
  250. }
  251.  
  252. function convert_duration_to_days ( duration,  
  253.     buf, idx )
  254. {
  255.     # The DURATION value is assumed to be of format [<n>H][<n>M][<n>S]
  256.     # subject to at least one of the terms being present.
  257.     # This was observed as Orage's subset of RFC 5545's DURATION.
  258.  
  259.     buf = 0
  260.     if ( ( idx = index( duration, "H" ) ) > 1 )
  261.     {
  262.         buf = substr( duration, 1, idx -1 ) * 60 * 60
  263.         duration = substr( duration, idx + 1 )
  264.     }
  265.     if ( ( idx = index( duration, "M" ) ) > 1 )
  266.     {
  267.         buf += ( substr( duration, 1, idx - 1 ) ) * 60
  268.         duration = substr( duration, idx + 1 )
  269.     }
  270.     if ( ( idx = index( duration, "S" ) ) > 1 )
  271.     {
  272.         buf += ( substr( duration, 1, idx - 1 ) )
  273.     }
  274.     return buf / ( 24 * 60 * 60 )
  275. }
  276.  
  277. function convert_time_to_days ( time,  
  278.     h, m, s )
  279. {
  280.     # A TIME value has format HHMMSS[Z]
  281.     h = substr( time, 1, 2 )
  282.     m = substr( time, 3, 2 )
  283.     s = substr( time, 5, 2 )
  284.     return ( ( ( ( h * 60 ) + m ) * 60 ) + s ) / ( 24 * 60 * 60 )
  285. }
  286.  
  287. function date_math_add_months ( date, increment,
  288.     my_return, m, y )
  289. {
  290.     msg( "D100", \
  291.         "date_math_add_months: started. date: " date ", increment: " increment )
  292.     m = substr( date, 5, 2 ) + increment
  293.     y = substr( date, 1, 4 ) + int( m / 12 )
  294.     m = m % 12
  295.     d = substr( date, 7 )
  296.     if ( length( y ) > 4 )
  297.         msg( "E", "date_math_add_months: year > 9999 generated while" \
  298.             " processing VEVENT before input record " NR )
  299.     if ( y < 1 )
  300.         msg( "E", "date_math_add_months: year < 1 generated while" \
  301.             " processing VEVENT before input record " NR )
  302.     if ( m == "02" && d == "29" && ! ( y % 4 == 0 && y % 100 != 0 ) \
  303.         || ( y % 400 == 0 ) \
  304.     )
  305.         return 0
  306.     my_return = sprintf( "%0.4d%0.2d%0.2d", y, m, d )
  307.     msg( "D100", "date_math_add_months: returning " my_return )
  308.     return my_return
  309. }
  310.  
  311. function date_math_add_weeks ( date, increment,
  312.     doe, my_return )
  313. {
  314.     msg( "D100", "date_math_add_weeks: started. date: " date ", increment: " \
  315.         increment )
  316.     doe = convert_date_to_doe( date ) + ( 7 * increment )
  317.     my_return = convert_doe_to_date( doe )
  318.     msg( "D100", "date_math_add_weeks: returning " my_return )
  319.     return my_return
  320. }
  321.  
  322. function date_math_add_years ( date, increment,
  323.     my_return, y )
  324. {
  325.     msg( "D100", "date_math_add_years: started. date: " date ", increment: " increment )
  326.     y = substr( date, 1, 4 ) + increment
  327.     if ( length( y ) > 4 )
  328.         msg( "E", "date_math_add_years: year > 9999 generated while" \
  329.             " processing data before input record " NR )
  330.     if ( y < 1 )
  331.         msg( "E", "date_math_add_years: year < 1 generated while" \
  332.             " processing data before input record " NR )
  333.     my_return = y substr( date, 5 )
  334.     msg( "D100", "date_math_add_years: returning " my_return )
  335.     return my_return
  336. }
  337.  
  338. function do_vevent ( \
  339.     end_doe, start_doe )
  340. {
  341.     start_doe = convert_date_to_doe( Dtstart_date )
  342.     if ( Dtend_date != "" )
  343.     {
  344.         end_doe = convert_date_to_doe( Dtend_date )
  345.     }
  346.     else
  347.     {   # Input had DURATION instead of DTEND
  348.         end_doe = int( start_doe + Dtstart_time_in_days + Duration_in_days )
  349.     }
  350.     msg( "D50", "do_vevent:"  \
  351.         "\n  start_doe: " start_doe \
  352.         "\n  end_doe: " end_doe \
  353.     )
  354.  
  355.     if ( in_output_range_by_doe( start_doe, end_doe ) ) write_vevent( )
  356. }
  357.  
  358. function do_vevents ( \
  359.     array, instance_end_date, instance_start_date, instance_start_dates,
  360.     n_dates, start_to_end_offset \
  361. )
  362. {
  363.     msg( "D10", "do_vevents: started" )
  364.  
  365.     # If input had DTEND, calculate DTSTART/DTEND offset in days
  366.     if ( Dtend_date != "" )
  367.         start_to_end_offset = convert_date_to_doe( Dtend_date ) \
  368.             - convert_date_to_doe( Dtstart_date )
  369.  
  370.     # Get the start date of events in the series that are in the date range
  371.     instance_start_dates = get_instance_start_dates( )
  372.  
  373.     # For each event in the recurring series ...
  374.     n_dates = split( instance_start_dates, array, " " )
  375.     msg( "D10", "do_vevents: n_dates: " n_dates )
  376.     for ( i = 1; i <= n_dates; i++ )
  377.     {
  378.         instance_start_date = array[ i ]
  379.         msg( "D20", "do_vevents: instance_start_date: " instance_start_date )
  380.         modify_dtstart_date( instance_start_date )
  381.         if ( Dtend_date != "" )
  382.         {
  383.             instance_end_date = convert_doe_to_date( \
  384.                 convert_date_to_doe( instance_start_date ) \
  385.                 + start_to_end_offset \
  386.             )
  387.             modify_dtend_date( instance_end_date )
  388.         }
  389.         write_vevent( )
  390.     }
  391. }
  392.  
  393. function get_begin_vevent ()
  394. {
  395.     for ( ; ; )
  396.     {
  397.         my_getline()
  398.         if ( $0 == "BEGIN:VEVENT" ) break
  399.         if ( $0 == "END:VCALENDAR" ) { print $0; exit 0 }
  400.         print $0
  401.     }
  402. }
  403.  
  404. function get_instance_start_dates ( \
  405.     byday, bydays, days_spanned, dtstart_doe, i, instance_end_date,
  406.     instance_end_doe, instance_start_date, instance_start_doe, j, j_max,
  407.     my_return, until_doe, weekday \
  408. )
  409. {
  410.     msg( "D40", "get_instance_start_dates: started" )
  411.     # From RFC 5545:
  412.     # If multiple BYxxx rule parts are specified then, after evaluating the
  413.     # specified FREQ and INTERVAL rule parts, the BYxxx rule parts are
  414.     # applied to the current set of evaluated occurrences in the following
  415.     # order: BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYHOUR,
  416.     # BYMINUTE, BYSECOND and BYSETPOS; then COUNT and UNTIL are evaluated.
  417.  
  418.     # From inspecting Orage's GUI, source code and sample export .ics files:
  419.     #   * RRULEs:
  420.     #     - FREQ is given and is one of DAILY, WEEKLY, MONTHLY or YEARLY
  421.     #     - One of UNTIL or COUNT may be given
  422.     #     - INTERVAL may not be given in which case it is defaulted to 1
  423.     #     - BYDAY may be given
  424.     #     - WKST may not be given in which case it is defaulted to MON
  425.     #   * One and only one DTSTART value is given
  426.     #   * One of DTEND or DURATION is given
  427.    
  428.     my_return = ""
  429.  
  430.     # Calculate some days-of-epoch
  431.     if ( Until_date == "" ) until_doe = 0
  432.     else until_doe = convert_date_to_doe( Until_date )
  433.     instance_start_doe = convert_date_to_doe( Dtstart_date )
  434.  
  435.     if ( Freq == "DAILY" )
  436.     {
  437.         # Calculate first VEVENT days
  438.         if ( Dtend_date != "" )
  439.         {
  440.             instance_end_date = Dtend_date
  441.             instance_end_doe = convert_date_to_doe( instance_end_date )
  442.         }
  443.         else
  444.         {   # Input had DURATION instead of DTEND
  445.             instance_end_doe = int( instance_start_doe + Dtstart_time_in_days \
  446.                 + Duration_in_days )
  447.         }
  448.         msg( "D50", "get_instance_start_dates, DAILY: " \
  449.             "\n  Dtstart_date: " Dtstart_date \
  450.             "\n  instance_start_doe: " instance_start_doe \
  451.             "\n  instance_end_doe: " instance_end_doe \
  452.         )
  453.  
  454.         # For each event in the series up to the end of the output time range
  455.         for ( i = 1; Count == 0 || i <= Count; )    # "Count == 0" is for UNTIL
  456.         {
  457.             if ( instance_start_doe > Output_date_range_end_doe ) break
  458.             if ( until_doe && instance_start_doe > until_doe ) break
  459.             skip = 0
  460.             if ( Byday_list != "" )
  461.             {     # For DAILY FREQs, this is a list of day names
  462.                 weekday = convert_doe_to_weekday( instance_start_doe )
  463.                 if ( index( Byday_list, weekday ) == 0 ) skip = 1
  464.             }
  465.             if ( ! skip &&
  466.                 instance_start_doe <= Output_date_range_end_doe &&
  467.                 instance_end_doe >= Output_date_range_start_doe \
  468.             ) my_return = my_return " " convert_doe_to_date( instance_start_doe )
  469.             # Prepare for next iteration
  470.             i++
  471.             instance_start_doe += Interval
  472.             instance_end_doe += Interval
  473.         }
  474.         msg( "D50", "get_instance_start_dates: returning: " my_return )
  475.         return my_return
  476.     }
  477.     else
  478.     {
  479.         # Calculate first VEVENT days and number of days spanned
  480.         instance_start_date = Dtstart_date
  481.         if ( Dtend_date != "" )
  482.         {
  483.             instance_end_date = Dtend_date
  484.         }
  485.         else
  486.         {   # Input had DURATION instead of DTEND
  487.             instance_end_doe = int( instance_start_doe + Dtstart_time_in_days \
  488.                 + Duration_in_days )
  489.             instance_end_date = convert_doe_to_date( instance_end_doe )
  490.         }
  491.         days_spanned = \
  492.             convert_date_to_doe( end_date ) - convert_date_to_doe( start_date )
  493.         msg( "D70", "get_instance_start_dates: " \
  494.             "\n  instance_start_date: " instance_start_date \
  495.             "\n  instance_end_date: " instance_end_date \
  496.             "\n  days_spanned: " days_spanned \
  497.         )
  498.  
  499.         # Always want the first instance if it is in the output time range
  500.         if ( in_output_range_by_date( instance_start_date, instance_end_date ) )
  501.             my_return = my_return " " instance_start_date
  502.  
  503.         # For each event in the series up to the end of the output time range
  504.         for ( i = 1; Count == 0 || i <= Count; i++ ) # "Count == 0" is for UNTIL
  505.         {
  506.             if ( Freq == "MONTHLY" )
  507.             {
  508.                 # Increment by month(s)
  509.                 last_instance_start_date = instance_start_date
  510.                 last_instance_end_date = instance_end_date
  511.                 instance_start_date = \
  512.                     date_math_add_months( instance_start_date, Interval )
  513.                 instance_end_date = \
  514.                     date_math_add_months( instance_end_date, Interval )
  515.  
  516.                 if ( Byday_list != "" )
  517.                     my_return = \
  518.                         my_return get_instance_start_dates_for_month_byday( \
  519.                             instance_start_date, days_spanned, until_doe \
  520.                         )
  521.                 else
  522.                 {
  523.                     for ( j = 1; instance_start_date == 0; )
  524.                     {   # Fell on non-existent Feb 29th
  525.                         instance_start_date = \
  526.                             date_math_add_months( last_instance_start_date, \
  527.                                 ++j * Interval )
  528.                         instance_end_date = \
  529.                             date_math_add_months( last_instance_end_date, \
  530.                                 j * Interval )
  531.                     }
  532.                     if ( instance_end_date == 0 )
  533.                     {   # Adjust non-existent 29 Feb to 1 Mar
  534.                         last_instance_doe = \
  535.                             convert_date_to_doe( last_instance_end_date )
  536.                         instance_end_date = \
  537.                             date_math_add_months( \
  538.                                 convert_doe_to_date( last_instance_end_doe + 1 ),
  539.                                 j * Interval \
  540.                             )
  541.                     }
  542.                     if ( in_output_range_by_date( instance_start_date, \
  543.                         instance_end_date ) \
  544.                     )
  545.                         my_return = my_return " " instance_start_date
  546.                 }
  547.             }
  548.             else if ( Freq == "WEEKLY" )
  549.             {
  550.                 # Increment by weeks(s)
  551.                 instance_start_date = \
  552.                     date_math_add_weeks( instance_start_date, Interval )
  553.                 instance_end_date = \
  554.                     date_math_add_weeks( instance_end_date, Interval )
  555.  
  556.                 if ( Byday_list != "" )
  557.                     # If instance start date is after the output range, earlier
  558.                     # in the week may not be
  559.                     my_return = my_return \
  560.                         get_instance_start_dates_for_week_byday( \
  561.                             instance_start_date, until_doe \
  562.                         )
  563.                 else
  564.                 {
  565.                     if ( in_output_range_by_date( instance_start_date, \
  566.                         instance_end_date ) \
  567.                     )
  568.                         my_return = my_return " " instance_start_date
  569.                 }
  570.             }
  571.             else
  572.             {   # YEARLY
  573.                 instance_start_date = \
  574.                     date_math_add_years( instance_start_date, Interval )
  575.                 instance_end_date = \
  576.                     date_math_add_years( instance_end_date, Interval )
  577.             }
  578.  
  579.             if ( instance_start_date > Output_date_range_end_date ) break
  580.         }
  581.         msg( "D50", "get_instance_start_dates: returning: " my_return )
  582.         return my_return
  583.     }
  584. }
  585.  
  586. function get_instance_start_dates_for_month_byday ( \
  587.     start_date, days_spanned, until_doe, \
  588.     \
  589.     byday, bydays, doe, month_end_doe, first_char, i, i_max, last_doe,
  590.     month_start_doe, my_return, negative, ordinal, weekday \
  591. )
  592. {
  593.     msg( "D50", "get_instance_start_dates_for_month_byday: called with: " \
  594.         "\n  start_date: " start_date \
  595.         "\n  days_spanned: " days_spanned \
  596.         "\n  until_doe: " until_doe \
  597.     )
  598.  
  599.     my_return = ""
  600.  
  601.     # For MONTHLY FREQs, Byday_list is a list of [+-]<ordinal><weekday>
  602.     i_max = split( Byday_list, bydays, "," )
  603.     for ( i = 1; i <= i_max; i++ )
  604.     {
  605.         byday = bydays[i]
  606.         first_char = substr( bydays[i], 1, 1 )
  607.         negative = 0
  608.         if ( first_char == "-" )
  609.         {
  610.             negative = 1
  611.             byday = substr( byday, 2 )
  612.         }
  613.         else if ( first_char == "+" )
  614.             byday = substr( byday, 2 )
  615.         weekday = substr( byday, length( byday ) - 1 )
  616.         ordinal = substr( byday, 1, length( byday ) - 2 )
  617.         month_end_doe = convert_date_to_doe( get_month_end_date( start_date ) )
  618.         if ( until_doe && until_doe < month_end_doe )
  619.             last_doe = until_doe
  620.         else last_doe = month_end_doe
  621.         month_start_doe = convert_date_to_doe( get_month_start_date( start_date ) )
  622.         occurrence = 0
  623.         msg( "D60", "get_instance_start_dates_for_month_byday: " \
  624.             "\n negative: " negative \
  625.             "\n weekday: " weekday \
  626.             "\n ordinal: " ordinal \
  627.             "\n month_start_doe: " month_start_doe \
  628.             "\n last_doe: " last_doe \
  629.         )
  630.         if ( negative )
  631.         {
  632.             # Search backwards from last day of month in output date range
  633.             for ( doe = last_doe; doe >= month_start_doe; doe-- )
  634.             {
  635.                 if ( convert_doe_to_weekday( doe ) == weekday )
  636.                 {
  637.                     if ( ++occurrence == ordinal )
  638.                     {
  639.                         if ( in_output_range_by_doe( doe, doe + days_spanned ) )
  640.                             my_return = my_return " " convert_doe_to_date( doe )
  641.                         break
  642.                     }
  643.                 }
  644.             }
  645.             msg( "D100", \
  646.                 "get_instance_start_dates_for_month_byday: my_return: " \
  647.                 my_return \
  648.             )
  649.         }
  650.         else
  651.         {
  652.             # Search forwards from first day of month to last day of output date
  653.             # range
  654.             for ( doe = month_start_doe; doe <= last_doe; doe++ )
  655.             {
  656.                 if ( convert_doe_to_weekday( doe ) == weekday )
  657.                 {
  658.                     if ( ++occurrence == ordinal )
  659.                     {
  660.                         if ( in_output_range_by_doe( doe, doe + days_spanned ) )
  661.                             my_return = my_return " " convert_doe_to_date( doe )
  662.                         break
  663.                     }
  664.                 }
  665.             }
  666.             msg( "D100", \
  667.                 "get_instance_start_dates_for_month_byday: my_return: " \
  668.                 my_return \
  669.             )
  670.         }
  671.     }
  672.     msg( "D50", \
  673.         "get_instance_start_dates_for_month_byday: returning: " my_return )
  674.     return my_return
  675. }
  676.  
  677. function get_instance_start_dates_for_week_byday ( start_date, until_doe,  \
  678.     doe, i, i_max, j, week_start_doe, my_return, weekday \
  679. )
  680. {
  681.     msg( "D50", "get_instance_start_dates_for_week_byday: called with: " \
  682.         "\n  start_date: " start_date \
  683.         "\n  until_doe: " until_doe \
  684.     )
  685.  
  686.     my_return = ""
  687.  
  688.     # For WEEKLY FREQs, Byday_list is a list of day names
  689.     i_max = split( Byday_list, bydays, "," )
  690.     for ( i = 1; i <= i_max; i++ )
  691.     {
  692.         weekday = bydays[i]
  693.         week_start_doe = get_week_start_doe( start_date )
  694.         for ( j = 0; j < 6; j++ )
  695.         {
  696.             doe = week_start_doe + j
  697.             if ( until_doe && doe > until_doe ) break
  698.             if ( doe > Output_date_range_end_doe ) break
  699.             if ( convert_doe_to_weekday( doe ) == weekday )
  700.             {
  701.                 my_return = my_return " " convert_doe_to_date( doe )
  702.                 break
  703.             }
  704.        
  705.         }
  706.     }
  707.     msg( "D50", \
  708.         "get_instance_start_dates_for_week_byday: returning: " my_return )
  709.     return my_return
  710. }
  711.  
  712. function get_month_end_date ( date, \
  713.     y, m, d )
  714. {
  715.     y = substr( date, 1, 4 )
  716.     m = substr( date, 5, 2 )
  717.     if ( m == "02" && leap_year( y ) ) d = 29
  718.     else d = Month_day_counts[m + 0]
  719.     return sprintf( "%d%02d%02d", y, m, d )
  720. }
  721.  
  722. function get_month_start_date ( date )
  723. {
  724.     return substr( date, 1, 6) "01"
  725. }
  726.  
  727. function get_week_start_doe ( date, \
  728.     doe, weekday )
  729. {
  730.     doe = convert_date_to_doe( date )
  731.     for ( i = 0; i < 7; i++ )
  732.     {
  733.         msg( "D100", "get_week_start_doe:" \
  734.             " convert_doe_to_weekday( " doe " - " i " ): " \
  735.             convert_doe_to_weekday( doe - i ) \
  736.         )
  737.         if ( convert_doe_to_weekday( doe - i ) == Wkst ) return doe - i
  738.     }
  739.     msg( "E", "get_week_start_doe: did not find WKST (" Wkst \
  740.         ") after looking back a full week" )
  741. }
  742.  
  743. function getopt(argc, argv, options,    thisopt, i)
  744. {
  745.     # From http://www.gnu.org/software/gawk/manual/gawk.html#Getopt-Function
  746.     # Modified to return option letter of any invalid option instead of ?
  747.     if (length(options) == 0)    # no options given
  748.         return -1
  749.      
  750.     if (argv[Optind] == "--") {  # all done
  751.         Optind++
  752.         _opti = 0
  753.         return -1
  754.     } else if (argv[Optind] !~ /^-[^: \t\n\f\r\v\b]/) {
  755.         _opti = 0
  756.         return -1
  757.     }
  758.     if (_opti == 0)
  759.         _opti = 2
  760.     thisopt = substr(argv[Optind], _opti, 1)
  761.     Optopt = thisopt
  762.     i = index(options, thisopt)
  763.     if (i == 0) {
  764.         if (Opterr)
  765.            printf("%c -- invalid option\n",
  766.                thisopt) > "/dev/stderr"
  767.         if (_opti >= length(argv[Optind])) {
  768.            Optind++
  769.            _opti = 0
  770.         } else
  771.             _opti++
  772.         # Local modification
  773.         #return "?"
  774.         return thisopt
  775.     }
  776.     if (substr(options, i + 1, 1) == ":") {
  777.         # get option argument
  778.         if (length(substr(argv[Optind], _opti + 1)) > 0)
  779.            Optarg = substr(argv[Optind], _opti + 1)
  780.         else
  781.            Optarg = argv[++Optind]
  782.         _opti = 0
  783.     } else
  784.         Optarg = ""
  785.     if (_opti == 0 || _opti >= length(argv[Optind])) {
  786.         Optind++
  787.         _opti = 0
  788.     } else
  789.         _opti++
  790.     return thisopt
  791. }
  792.  
  793. function get_vevent ( \
  794.     i, n_lines \
  795. )
  796. {
  797.     Dtend_date = ""
  798.     Dtstart = ""
  799.     Duration_days = 0
  800.     Recurrence = 0
  801.     n_lines = 0
  802.     Lines[++n_lines] = $0
  803.     while ( $0 != "END:VEVENT" )
  804.     {
  805.         my_getline()
  806.         if ( $1 == "RRULE")
  807.         {
  808.             Recurrence = 1
  809.             parse_rrule( $0 )
  810.         }
  811.         else if ( $1 ~ /^X-ORAGE-/ )
  812.             continue
  813.         else if ( $1 == "ATTACH" )
  814.             continue
  815.         else
  816.         {
  817.             Lines[ ++n_lines ] = $0
  818.             if ( $1 == "ACTION" )
  819.             {
  820.                 if ( Audio_alarm )
  821.                     Lines[ n_lines ] = "ACTION:AUDIO"
  822.                 else if ( Visual_alarm )
  823.                     Lines[ n_lines ] = "ACTION:DISPLAY"
  824.                 else if ( $1 == "PROCEDURE" )
  825.                     Lines[ n_lines ] = "ACTION:AUDIO" # Sane default
  826.             }
  827.             else if ( $1 == "DTEND" )
  828.             {
  829.                 parse_dtend( $0 )
  830.                 Dtend_line = n_lines
  831.             }
  832.             else if ( $1 == "DTSTART" )
  833.             {
  834.                 parse_dtstart( $0 )
  835.                 Dtstart_line = n_lines
  836.             }
  837.             else if ( $1 == "DURATION" )
  838.                 parse_duration( $0 )
  839.         }
  840.     }
  841.     if ( Debug_level )
  842.     {
  843.         msg( "D1", "get_vevent: Lines:" )
  844.         for ( i = 1; i <= n_lines; i++ )
  845.             msg( "D", i " " Lines[ i ] )
  846.         msg( "D10", "get_vevent: Dtstart_date: " Dtstart_date \
  847.             ", Dtend_date: " Dtend_date )
  848.     }
  849.     return n_lines
  850. }
  851.  
  852. function in_output_range_by_date ( start_date, end_date )
  853. {
  854.     if ( start_date <= Output_date_range_end_date &&
  855.         end_date >= Output_date_range_start_date \
  856.     )
  857.         return 1
  858.     else
  859.         return 0
  860. }
  861.  
  862. function in_output_range_by_doe ( start_doe, end_doe )
  863. {
  864.     if ( start_doe <= Output_date_range_end_doe &&
  865.         end_doe >= Output_date_range_start_doe ) return 1
  866.     else return 0
  867. }
  868.  
  869. function initialise ( \
  870.     array, cmdline, i, now, opt )
  871. {
  872.     Version = "0.1"
  873.  
  874.     # Get last component of path by which this script called
  875.     # Has to be done now in case needed for the -h help option
  876.     getline cmdline < "/proc/self/cmdline"
  877.     split( cmdline, array, "\0" )
  878.     My_name = substr( array[3], match( array[3], /\/[^\/]*$/ ) + 1 )
  879.  
  880.     # Parse command line
  881.     Audio_alarm = 0
  882.     Opterr = 0    # Suppress getopt( ) error messages
  883.     Optind = 1    # Skip ARGV[0]
  884.     Debug_level = 0
  885.     Visual_alarm = 0
  886.     while ( ( opt = getopt(ARGC, ARGV, "ad:hVv")) != -1)
  887.     {
  888.         msg( "D", "initialise: opt: " opt )
  889.         if ( opt == "a" )
  890.         {
  891.             Audio_alarm = 1
  892.             Visual_alarm = 0
  893.         }
  894.         else if ( opt == "d" )
  895.         {
  896.             Debug_level = Optarg
  897.         }
  898.         else if ( opt == "h" )
  899.         {
  900.             usage( "verbose" )
  901.             exit 0
  902.         }
  903.         else if ( opt == "V" )
  904.         {
  905.             msg( "I", My_name " version " Version )
  906.             exit 0
  907.         }
  908.         else if ( opt == "v" )
  909.         {
  910.             Audio_alarm = 0
  911.             Visual_alarm = 1
  912.         }
  913.         else
  914.         {
  915.             usage( "quiet" )
  916.             msg( "E", "invalid command line option: " opt )
  917.         }
  918.     }
  919.     for ( i = 1; i < Optind; i++ ) ARGV[i] = ""
  920.  
  921.     # Log startup
  922.     "date +%Y/%m/%d@%H:%M:%S" | getline now
  923.     cmdline = substr( cmdline, length( array[1] ) + length( array[2] ) + 3 )
  924.     gsub( "\0", " ", cmdline )
  925.     msg( "I", "Started version " Version " at " now " with command line " cmdline )
  926.  
  927.     # Set up output date range
  928.     "date --date='-1 month' +%Y%m%d" | getline Output_date_range_start_date
  929.     Output_date_range_start_doe = \
  930.         convert_date_to_doe( Output_date_range_start_date )
  931.     "date --date='+2 month' +%Y%m%d" | getline Output_date_range_end_date
  932.     Output_date_range_end_doe = convert_date_to_doe( Output_date_range_end_date )
  933.     msg( "D1", "initialise: output date range: " Output_date_range_start_date \
  934.         " to " Output_date_range_end_date )
  935.     msg( "D10", "initialise: output date range (doe): " \
  936.         Output_date_range_start_doe " to " Output_date_range_end_doe )
  937.  
  938.     # Misc. variable initialisation
  939.     split( "31,28,31,30,31,30,31,31,30,31,30,31", Month_day_counts, "," )
  940.     split( "SU,MO,TU,WE,TH,FR,SA", Weekdays, "," )
  941. }
  942.  
  943. function leap_year( y )
  944. {
  945.     return ( ( y % 4 ) == 0 && ( y % 100 ) != 0 )
  946. }
  947.  
  948. function modify_dtend_date ( dtend_date, dtend_line )
  949. {
  950.     Lines[dtend_line] = sub( /:.*T/, ":" dtend_date "T", Lines[dtend_line] )
  951. }
  952.  
  953. function modify_dtstart_date ( start_date )
  954. {
  955.     sub( /:.*T/, ":" start_date "T", Lines[Dtstart_line] )
  956. }
  957.  
  958. function msg ( msg_class, msg_text,
  959.     exit_flag, my_debug_level )
  960. {
  961.     exit_flag = 0
  962.     if ( msg_class ~ /^D/ )
  963.     {
  964.         if ( ! Debug_level ) return
  965.         if ( length( msg_class ) > 1 ) my_debug_level = substr( msg_class, 2 )
  966.         else my_debug_level = 1 # Sane default
  967.         if ( my_debug_level > Debug_level ) return
  968.         msg_text = "DEBUG: " msg_text
  969.     }
  970.     else if ( msg_class == "I" )
  971.     {
  972.     }
  973.     else if ( msg_class == "W" )
  974.     {
  975.         msg_text = "WARNING: " msg_text
  976.     }
  977.     else if ( msg_class == "E" )
  978.     {
  979.         msg_text = "ERROR: " msg_text
  980.         exit_flag = 1
  981.     }
  982.     else
  983.     {
  984.         msg_text =  "ERROR: msg: called with invalid message class " msg_class \
  985.             "(and message text: " msg_text " )"
  986.         exit_flag = 1
  987.     }
  988.     print msg_text > "/dev/stderr"
  989.     if ( exit_flag ) exit 1
  990. }
  991.  
  992. function my_getline ()
  993. {
  994.     if ( getline <= 0 ) {
  995.         m = "unexpected EOF or error"
  996.         m = (m ": " ERRNO)
  997.         print m > "/dev/stderr"
  998.         exit 1
  999.     }  
  1000. }
  1001.  
  1002. function parse_dt ( line,  
  1003.     array )
  1004. {
  1005.     split( line, array, ":" )
  1006.     return array[2]
  1007. }
  1008.  
  1009. function parse_dt_date ( dt,  
  1010.     array )
  1011. {
  1012.     split( dt, array, "T" )
  1013.     return array[1]
  1014. }
  1015.  
  1016. function parse_dt_time ( dt,  
  1017.     array )
  1018. {
  1019.     split( dt, array, "T" )
  1020.     return array[2]
  1021. }
  1022.  
  1023. function parse_dtend ( line )
  1024. {
  1025.     Dtend = parse_dt( line )
  1026.     Dtend_date = parse_dt_date( Dtend )
  1027.     Dtend_time = parse_dt_time( Dtend )
  1028.     msg( "D30", "parse_dtend: Dtend_date is " Dtend_date ". Dtend_time is " \
  1029.         Dtend_time )
  1030. }
  1031.  
  1032. function parse_dtstart ( line )
  1033. {
  1034.     dtstart = parse_dt( line )
  1035.     Dtstart_date = parse_dt_date( dtstart )
  1036.     Dtstart_time = parse_dt_time( dtstart )
  1037.     Dtstart_time_in_days = convert_time_to_days( Dtstart_time )
  1038.     msg( "D", "parse_dtstart: Dtstart_date: " Dtstart_date \
  1039.         ", Dtstart_time: " Dtstart_time \
  1040.         ", Dtstart_time_in_days: " Dtstart_time_in_days )
  1041. }
  1042.  
  1043. function parse_duration ( line,
  1044.     array, duration, duration_time )
  1045. {
  1046.     split( line, array, ":" )
  1047.     duration = array[2]
  1048.     if ( substr( duration, 1, 1 ) != "P" )
  1049.         msg( "E", "DURATION value does not begin with P: " line )
  1050.     duration = substr( duration, 2 )
  1051.     if ( split( duration, array, "T" ) > 1 )
  1052.     {
  1053.         # Orage does not set durations in weeks
  1054.         Duration_days = array[1]
  1055.         duration = array[2]
  1056.     }
  1057.     else
  1058.     {
  1059.         Duration_days = ""
  1060.         Duration = array[1]
  1061.     }
  1062.     duration = convert_duration_to_days( duration )
  1063.     Duration_days = Duration_days + duration
  1064.     msg( "D", "parse_duration: Duration_days:" Duration_days )
  1065. }
  1066.  
  1067. function parse_rrule ( line,  
  1068.     i, keyword, keyvalue, n_rule_parts, rule_parts )
  1069. {
  1070.     # Orage supports:
  1071.     # Frequency: Daily, Weekly, Monthly, Yearly
  1072.     # Interval: 1,2,3 ...
  1073.     # Repeat: Forever, Count, Until
  1074.     # Weekdays (except daily frequency)
  1075.     # Which day (monthly and yearly frequency only): + or - 1,2,3 ...
  1076.    
  1077.     # Initialise
  1078.     Byday_list = ""
  1079.     Bymonth_list = ""
  1080.     Bymonthday_list = ""
  1081.     Bysetpos_list = ""
  1082.     Byyearday_list = ""
  1083.     Count = 0
  1084.     Interval = 1
  1085.     Until = ""
  1086.     Until_date = ""
  1087.     Wkst = "MO"
  1088.  
  1089.     # Parse
  1090.     # TODO: why treat FREQ differently from the others?
  1091.     n_rule_parts = split( line, rule_parts, /[:;]/ )
  1092.     Freq = gensub( /FREQ=/, "", 1, rule_parts[2] )
  1093.     for ( i = 3; i <= n_rule_parts; i++ )
  1094.     {
  1095.         keyword = gensub( /=.*/, "", 1, rule_parts[i] )
  1096.         keyvalue = gensub( /.*=/, "", 1, rule_parts[i] )
  1097.         if ( keyword == "BYDAY" ) Byday_list = keyvalue
  1098.         else if ( keyword == "BYMONTH" ) Bymonth_list = keyvalue
  1099.         else if ( keyword == "BYMONTHDAY" ) Bymonthday_list = keyvalue
  1100.         else if ( keyword == "BYSETPOS" ) Bysetpos_list = keyvalue
  1101.         else if ( keyword == "BYYEARDAY" ) Byyearrday_list = keyvalue
  1102.         else if ( keyword == "COUNT" ) Count = keyvalue
  1103.         else if ( keyword == "INTERVAL" ) Interval = keyvalue
  1104.         else if ( keyword == "UNTIL" )
  1105.         {
  1106.             Until = keyvalue
  1107.             Until_date = substr( Until, 1, 8 )
  1108.         }
  1109.         else if ( keyword == "WKST" )
  1110.             Wkst = keyvalue
  1111.         else
  1112.             msg( "E", "unrecognised RRULE keyword " keyword " in " line )
  1113.     }
  1114.  
  1115.     msg( "D",
  1116.     "parse_rrule:" \
  1117.     "\n  Freq = " Freq \
  1118.     "\n  Byday_list = " Byday_list \
  1119.     "\n  Bymonth_list = " Bymonth_list \
  1120.     "\n  Bymonthday_list = " Bymonthday_list \
  1121.     "\n  Bysetpos_list = " Bysetpos_list \
  1122.     "\n  Byyearday_list = " Byyearday_list \
  1123.     "\n  Count = " Count \
  1124.     "\n  Interval = " Interval \
  1125.     "\n  Until = " Until \
  1126.     "\n  Until_date = " Until_date \
  1127.     "\n  Wkst = " Wkst )
  1128.  
  1129. }
  1130.  
  1131. function usage ( verbosity )
  1132. {
  1133.     msg( "I", "Usage:" \
  1134.         "\n  " My_name " [-a] [-d debug_level] [-h] [-V] [-v]" \
  1135.     )
  1136.     if ( verbosity == "verbose" )
  1137.         msg( "I", "  Where:" \
  1138.             "\n    -a sets all alarms to audio" \
  1139.             "\n    -d sets the debug level" \
  1140.             "\n       1 to 100 gives progressively more detail" \
  1141.             "\n    -h prints this help message and exits" \
  1142.             "\n    -v sets all alarms to visual" \
  1143.             "\n    -V prints the version number and exits" \
  1144.             "\n  If both -a and -v are given, the last is effective." \
  1145.             "\n  Reads an Orage export .ics file from stdin." \
  1146.             "\n  Writes a .ics file to suit gnokii to stdout." \
  1147.             "\n  Writes logging to stderr." \
  1148.         )
  1149.        
  1150. }
  1151.  
  1152. function write_vevent ( n_lines,  
  1153.     i )
  1154. {
  1155.     msg( "D10", "write_vevent: writing event" )
  1156.     for ( i = 1; i <= N_lines; i++ ) print Lines[i]
  1157. }
  1158.  
  1159. BEGIN {
  1160.     FS = "[:;]"
  1161.     initialise()
  1162.     for ( ; ; )
  1163.     {
  1164.         # Replicate input to output until the next VEVENT or EoF
  1165.         get_begin_vevent()
  1166.  
  1167.         # Input a single VEVENT
  1168.         N_lines = get_vevent()
  1169.  
  1170.         # Output any VEVENTs in the time range
  1171.         if ( Recurrence ) do_vevents( )
  1172.         else do_vevent( )
  1173.     }
  1174. }
RAW Paste Data