MBS FileMaker Advent calendar - Door 18 - ics-Files
Day 18 - ics-Files |
In door 16 we took care of the fact that we can enter appointments in our calendar, but what do we do if our monkey wants to share an appointment with someone, e.g. as an attachment for an appointment confirmation. We can also realize this by assembling an ics file that contains the data for an appointment. As with the business card that we embedded in the QR code, an ics file also has a specific form, where some data is required and some is optional. But let's take a look at the structure of such a file by exporting an existing appointment from the calendar and opening it in an editor. It is an event that takes place on 24th December 2024 from 17:00 to 23:30 and has the title Christmas Eve Dinner. It takes place in Home, has a further description and a link to a website. And this is what it looks like in the editor:
BEGIN:VCALENDAR CALSCALE:GREGORIAN PRODID:-//Apple Inc.//macOS 13.4.1//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Europe/Berlin BEGIN:DAYLIGHT DTSTART:19810329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU TZNAME:CEST TZOFFSETFROM:+0100 TZOFFSETTO:+0200 END:DAYLIGHT BEGIN:STANDARD DTSTART:19961027T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU TZNAME:CET TZOFFSETFROM:+0200 TZOFFSETTO:+0100 END:STANDARD END:VTIMEZONE BEGIN:VEVENT CREATED:20241212T205429Z DESCRIPTION:Prepare food on December 23rd DTEND;TZID=Europe/Berlin:20241224T233000 DTSTAMP:20241212T205735Z DTSTART;TZID=Europe/Berlin:20241224T170000 LAST-MODIFIED:20241212T205713Z LOCATION:Home SEQUENCE:0 SUMMARY:Christmas Dinner TRANSP:OPAQUE UID:809C6AEE-B99E-4DBB-A29F-4223314288A3 URL;VALUE=URI:www.xyz.com BEGIN:VALARM ACTION:NONE TRIGGER;VALUE=DATE-TIME:19760401T005545Z END:VALARM END:VEVENT END:VCALENDAR
Don't panic, you won't need as much information as you see here to create the appointment, but let's take a look at which of it is relevant for us.
The content of the appointment starts with BEGIN:VCALENDAR and ends with END:VCALENDAR, with the information about the appointment in between.
The next line: CALSCALE:GREGORIAN specifies, as you have probably already guessed, the type of calendar. In our case, the Gregorian calendar. Then comes the version. This must be present in every date specification so that it is known how the individual details are to be interpreted. This can simply remain at 2.0.
The PRODID key describes who created the file. In the event you see here, you can see that the calendar app created this event. In our example, we pass the company name afterwards. Then we get down to the details, the data for the actual appointment. Here, our area is always enclosed by BEGIN:VEVENT and END:VEVENT. Such an area must appear at least once in our appointment. Now let's take a look at what's inside this area. Our example appointment is an appointment that has a start and end time. The end time has the key DTEND. We can see that in addition to the character string containing the date and time, the time zone is also specified. In our case, the time that applies in my case, as I live in Germany, is Berlin. If you want to find out more about the individual time zones, you can find this information on the website timezones.de, for example.
Now comes the timestamp and it has a very specific structure. First comes a 4-digit year, then the month, which takes up 2 digits, and the day, which also has 2 digits. This is followed by a T that separates the time from the date. The individual entries for the time are each two characters long and are made up of hours, minutes and seconds. The timestamp therefore has the structure: yyyymmddThhmmss. August 19, 2025 at 9:00 a.m. would therefore receive the following entry: 20250819T090000.
We also have the entry DTSTART in this date. This determines the start time of the event and has the same structure. We see something similar in the time stamps, which tell us when the event was created (CREATED), when it was last modified (LAST-MODIFIED), and when this file was exported or created (DTSTAMP). DTSTAMP is a time specification that must be included in the date. In contrast to the start and end time, we find a Z at the end of the time stamp.Now we come to a few more useful keys. First, we have the LOCATION key to which we can pass a location or address. Under the DESCRIPTION key, you can describe the appointment further. This content is then displayed as a note for the appointment. You can also use the URL key to specify a URL that belongs to the organizer's or event's website, for example. Another important key is SUMMARY. This describes the title of our event.
The keys do not have to remain in this order, but can be swapped, as we do in our example, so that you can find the order that makes the most sense for you.
We have already created our fields at door 16 and so we only have to create a button in the popover that starts the script that we now want to write. The beginning of our appointment is always the same and we put it together as text. The content of our file will later be in the variable $Content
Set Variable [ $Company ; Value: "Monkey the Elf" ] Set Variable [ $Start ; Value: "BEGIN:VCALENDAR¶CALSCALE:GREGORIAN¶VERSION:2.0¶METHOD:PUBLISH¶PRODID:" & $Company & "¶BEGIN:VEVENT" ] Set Variable [ $Content ; Value: $Start ]
Now we can enter the title of our calendar entry. This has the key SUMMARY
Set Variable [ $Title ; Value: "¶SUMMARY:" & Data::Appointment_Title & "¶" ] Set Variable [ $Content ; Value: $Content & $Title ]
The description, the URL and the location of an event are not mandatory, which is why we first check whether these values have been set in our database and only then we add the key with the corresponding value.
If [ Data::Appointment_Notes ≠ "" ] Set Variable [ $Description ; Value: "DESCRIPTION:" & Data::Appointment_Notes & "¶" ] Set Variable [ $Content ; Value: $Content & $Description ] End If # If [ Data::Appointment_URL ≠ "" ] Set Variable [ $URL ; Value: "URL;VALUE=URI:" & Data::Appointment_URL & "¶" ] Set Variable [ $Content ; Value: $Content & $URL ] End If # If [ Data::Appointment_Location ≠ "" ] Set Variable [ $Location ; Value: "LOCATION:" & Data::Appointment_Location & "¶" ] Set Variable [ $Content ; Value: $Content & $Location ] End If
Now we come to the part that is a bit tricky, because we have to differentiate between an all-day appointment and an appointment with a start and end time. This is because the composition of the time differs slightly here. If the Appointment_All Day field is set to 1, we jump to the TimeAllday script that compiles the all-day time.
If [ Data::Appointment_All Day = 1 ] Perform Script [ Specified: From list ; "TimeAllday" ; Parameter: ]
We compose the date as we discussed earlier: four characters year, two characters month and two characters day. We get these values from the date field using the corresponding FileMaker functions. But be careful: If we have a day or a month with a value less than 10, the function only returns one character and we have to add the preceding zero.
Set Variable [ $Year ; Value: Year ( Data::Appointment_Start ) ] # If [ Length ( Month ( Data::Appointment_Start ) ) ≠ 2 ] Set Variable [ $Month ; Value: "0" & Month ( Data::Appointment_Start ) ] Else Set Variable [ $Month ; Value: Month ( Data::Appointment_Start ) ] End If # If [ Length ( Day ( Data::Appointment_Start ) ) ≠ 2 ] Set Variable [ $Day ; Value: "0" & Day ( Data::Appointment_Start ) ] Else Set Variable [ $Day ; Value: Day ( Data::Appointment_Start ) ] End If Set Variable [ $StartDay ; Value: $Year & $Month & $Day ] Set Variable [ $Beginning ; Value: "DTSTART;VALUE=DATE:" & $StartDay ] Set Variable [ $Time ; Value: $Beginning & "¶" ]
Now we have to differentiate whether it is an all-day appointment lasting several days or whether it is only one day with an all-day appointment. To do this, we subtract the end date from the start date and obtain the day difference. If we get a value greater than 0 here, it is an all-day event that lasts several days. If we only specify the end date in this case, this is often interpreted incorrectly and, for example, one day too little is displayed for the event in the calendar. For this reason, we would like to specify the DURATION in this case. This is made up as follows: First we write a P and then comes the duration of the event in days or weeks. We can see whether it is days or weeks by the character after the duration. If we enter weeks, there is a W, if we enter days, as in our case, there is a D. You can also combine the times by writing them down one after the other. For example, the duration of 2 weeks and 3 days would be described with this expression: P2W3D.
Because our Appointment_Start and Appointment_End fields are of the timestamp type, if we simply subtract them from each other we would get the seconds between the two times, but we need the days. We get these if we subtract two date values from each other. For this reason, we convert the two time points into dates and then subtract them from each other.
Set Variable [ $DayDifference ; Value: Date ( Month ( Data::Appointment_End ) ; Day ( Data::Appointment_End ) ; Year ( Data::Appointment_End ) ) - Date ( $Month ; $Day ; $Year) ]
If the difference is greater than 0, then our appointment is for several days and we want to specify the duration.
If [ $DayDifference>0 ] Set Variable [ $TimeDifference ; Value: "DURATION:P" & $DayDifference+1 & "D" ] Set Variable [ $Time ; Value: $Time & $TimeDifference ]
If the difference is zero, then the appointment will only take place for the whole day on this one day and we will also enter the start date as the end date.
Else If [ $DayDifference=0 ] Set Variable [ $EndDay ; Value: "DTEND;VALUE=DATE:" & $StartDay ] Set Variable [ $Time ; Value: $Time & $EndDay ]
If the day difference is neither greater than 0 nor equal to zero, then it must be less than 0, i.e. our start time is after the specified end time. In this case, we simply want to swap the data in our database and we call the same time script again and process the result that this call returns.
Else Set Variable [ $S ; Value: Data::Appointment_End ] Set Variable [ $E ; Value: Data::Appointment_Start ] Set Field [ Data::Appointment_Start ; $S ] Set Field [ Data::Appointment_End ; $E ] Perform Script [ Specified: From list ; "TimeAllday" ; Parameter: ] Set Variable [ $Time ; Value: Get(ScriptResult) ] # result End If
Our script now returns the string with the time information to the calling main script.
Exit Script [ Text Result: $Time ]
We have now seen this for all-day appointments, but if it is not an all-day appointment, then we call the TimeLimited script.
Else Perform Script [ Specified: From list ; "TimeLimited" ; Parameter: ] End If
Here, not only the date is entered in the specific timestamp, but also the time. The principle is exactly the same as before. First we have the date in the structure YYYYMMDD and then comes the time with HHMMSS. As the seconds are not so important for an appointment, we simply leave them out and write 00 for the seconds instead. We have our DTSTART key again. We then add the desired time zone TZID=Europe/Berlin to this, and then our date is put together. This is followed by a separating T and then the hours, minutes and our 00 for the seconds.
# TimeLimited Set Variable [ $Year_Start ; Value: Year ( Data::Appointment_Start ) ] # If [ Length ( Month ( Data::Appointment_Start ) ) ≠ 2 ] Set Variable [ $Month_Start ; Value: "0" & Month ( Data::Appointment_Start ) ] Else Set Variable [ $Month_Start ; Value: Month ( Data::Appointment_Start ) ] End If # If [ Length ( Day ( Data::Appointment_Start ) ) ≠ 2 ] Set Variable [ $Day_Start ; Value: "0" & Day ( Data::Appointment_Start ) ] Else Set Variable [ $Day_Start ; Value: Day ( Data::Appointment_Start ) ] End If If [ Length ( Hour ( Data::Appointment_Start ) ) ≠ 2 ] Set Variable [ $Hour_Start ; Value: "0" & Hour( Data::Appointment_Start ) ] Else Set Variable [ $Hour_Start ; Value: Hour ( Data::Appointment_Start ) ] End If # If [ Length ( Minute ( Data::Appointment_Start ) ) ≠ 2 ] Set Variable [ $Minute_Start ; Value: "0" & Minute ( Data::Appointment_Start ) ] Else Set Variable [ $Minute_Start ; Value: Minute ( Data::Appointment_Start ) ] End If Set Variable [ $Beginning ; Value: "DTSTART;TZID=Europe/Berlin:" & $Year_Start & $Month_Start & $Day_Start & "T" & $Hour_Start & $Minute_Start & "00" ] Set Variable [ $Time ; Value: $Beginning&"¶" ]
We do exactly the same for DTEND. And, as seen in the other script, we also return our time as text to our main script. The time is then added to the previous content here.
Set Variable [ $Time ; Value: Get(ScriptResult) ] Set Variable [ $Content ; Value: $Content & $Time & "¶" ]
Now we still need the time at which we exported the appointment, or in our case created it (DTSTAMP). This timestamp has the following form: yymmddThhmmssZ Here, too, we neglect the seconds and enter them as 00.
… Set Variable [ $CurrentTime ; Value: "DTSTAMP:" & $Year_Current & $Month_Current & $Day_Current & "T" & $Hour_Current & $Minute_Current & "00Z" ] Set Variable [ $Content ; Value: $Content & $CurrentTime & "¶" ]
In the content of our appointment, the final boundaries of our areas are still missing. We will add these.
Set Variable [ $Content ; Value: $Content & "END:VEVENT¶END:VCALENDAR" ]
We have now compiled our text and want to save this content in a path on the desktop. First, we create this path as we have already shown in other doors.
Set Variable [ $Desk ; Value: MBS("Folders.UserDesktop") ] Set Variable [ $Path ; Value: MBS("Path.AddPathComponent"; $Desk; "Appointment" & ".ics") ]
Then we use the functions from the BinaryFile component to create a text file of type ics. We then use "BinaryFile.Create" to create such a text file by specifying the path we have just determined. We write the content to the file using the "BinaryFile.WriteText" function. In the parameters of this function, in addition to the reference number obtained from the previous function and the content stored in the $Content variable, we also specify the encoding of the text. We select UTF-8. With "BinaryFile.Close" we close the file again and it can be used.
Set Variable [ $File ; Value: MBS("BinaryFile.Create"; $Path) ] Set Variable [ $r ; Value: MBS("BinaryFile.WriteText"; $File; $Content; "UTF-8") ] Set Variable [ $r ; Value: MBS("BinaryFile.Close"; $File) ]
Now we can create our appointment files with the push of a button. I hope to see you again tomorrow for the next door.
17 👈 | 18 of 24 | 👉 19 |