A Custom Report Script by Internal Scripting

The Internal Scripts for this report are in two scripts in the "CreateReportTutorial.gplug" extension. The main script is in the "Generation Ages IS.gcscpt" script. Subroutines needed by that script are in the "ISAgeModule.gcscpt" module. Those subroutines could be moved to the start of the main script, but having them in separate script both simplifes the main script and demonstrates Load option for Internal Scripts. To run this tutorial report, select "Custom Report Tutorial" from the Extensions menu and then choose the "Internal Script Custom Report" option.

Internal Scripting is the recommended language for sripting GEDitCOM II. It is a fairly simple language, but has some unusual syntax (driven by methods used to program the language). Much of the odd syntax is caused by Internal Scripting having distiguishable variables and scripting objects. Variables correspond to strings and numbers. Objects correspond to Internal Scripting objects defined in the Internal Scripting dictionary (see the GEDitCOM II help on the "Dictionary of Internal Scripting Objects.")

Variables can be assigned to expressions of other variables, used in conditionals, etc.; in other words that are like variables in other languages. Objects are created by other methods (often by "Create" commands or the "get" command). Once you have an object, you can read its properties or get other objects related to that object. Variable and object names are case sensitive. Although not required, Internal Scripts should start all variable names with number sign ("#"). The GEDitCOM Editor uses the number sign to color code variables in Internal Scripts. Furthermore, some Internal Scripting commands require input of a variable name and those commands may require that variable name to start in "#". Another difference in Internal Scripts is that all variables and scripting objects are global variables (if needed, this behavoir can be changed for selected subroutine calls).

You can consult the GEDitCOM Editor help for full documentation of Internal Scripting methods, variables, objects, and commands. Another way to learn Internal Scripting is to compare this sample script to the same script in another langauge. For example, many people are familiar with Python. By looking at each section below and then comparing to corresponding sections of a Python script, one can see translations of a more standard langauge into the syntax used by Internal Scripts.

Main Script

Listing 1
! Preamble
Load ISAgeModule

! Script name and version requirement
#scriptName="Generation Ages to Report (IS)"
PreChecks #scriptName,3.1,1,1,1
gcapp.get gdoc,frontDocument

! choose all or currently selected family records
#msg="Get report for All or just Selected family records"
UserOption "#whichOnes",#msg,"","All","Cancel","Selected"
if #whichOnes="Cancel"
  exit
endif

! Get of list of the choosen family records
if #whichOnes="All"
  gdoc.get fams,"families"
else
  gdoc.get fams,"selectedRecordsFAM"
endif

! No report if no family records were found
if @fams.count=0
  UserOption "#var","No family records were selected","","OK"
  exit
endif

! Collect all report data in a subroutine
GoSub CollectAges

! write to report and then done
GoSub WriteToReport

Listing 1 shows the main report script, This listing omits comments at the start of the script you can see in the GEDitCOM Editor. All good scripts should start with comments that describe its function along with any other relevant details.

The Preamble section first loads the subroutines in "ISAgeModule.gcscpt" to the start of the main script. The preamble continues by verifying the script can run. All the work is done by the PreChecks scripting commdand (note: all scripting commands are documented in the GEDitCOM Editor help). This command checks that a document is open and if the current GEDitCOM II (or GEDitCOM) version is new enough for this script. If the checks fail, the script exits with an error message. If it works, gcapp will be set as a scripting object for the main application object. Finally, the get command sets gdoc using the FrontDocument object attribute of an application object. This structure is an example of Internal Scripting syntax for scripting objects. In effect, this command results in

	gdoc = gcapp.FrontDocument.

but scripting objects cannot be assigned like regular variables. The get command achieves the same programming goal in Internal Scripts. The get command "gets" the frontDocument object in gcapp and sets scripting object gdoc to that object.

The next section chooses and finds the records used for the report. The choosing is done with the UserOption command. Once the choice is made, the records to be analyzed are loaded into a list in fams using a get command with the appropriate document object relation. If no family records were found, the user is alerted and the script quits. The syntax @fams.count starting with '@' sign fetches a string or numeric property of an object. This syntax can be used in expressions, conditionals, and with variables.

The remainder of the script does the bulk of the work in two subrouties defined in the ISAgeModule.gcscpt script and described in the next two sections


CollectAges() Subroutine

Listing 2
! collect data for te generation ages report
Sub CollectAges
  ! initialize counters
  #numHusbAge=0
  #sumHusbAge=0
  #numFathAge=0
  #sumFathAge=0
  #numWifeAge=0
  #sumWifeAge=0
  #numMothAge=0
  #sumMothAge=0
  
  ! progress reporting interval
  #fractionStepSize=0.01
  #nextFraction=0.01
  Repeat "#i",0,@fams.count-1
    fams.#i.get fam
    fam.get husb,"husband"
    fam.get wife,"wife"
    #mdate=@fam.marriageSDN

    ! read parent birthdates
    #hbdate=0
    #wbdate=0
    ifDef husb
      #hbdate=@husb.birthSDN
    endif
    ifDef wife
      #wbdate=@wife.birthSDN
    endif
    
    ! need at least one birth date
    if #hbdate+#wbdate=0
      continue
    endif
    
    ! spouse ages at marriage
    if #mdate>0:
      if #hbdate>0:
        #sumHusbAge+=(#mdate-#hbdate)/365.25
        #numHusbAge+=1
      endif
      
      if #wbdate>0:
        #sumWifeAge+=(#mdate-#wbdate)/365.25
        #numWifeAge+=1
      endif
    endif
    
    ! spouse ages when children were born
    fam.get chil,"children"
    Repeat "#c",0,@chil.count-1
      chil.#c.get child
      #cbdate=@child.birthSDN
      if #cbdate>0
        if #hbdate>0:
          #sumFathAge+=(#cbdate-#hbdate)/365.25
          #numFathAge+=1
        endif
        if #wbdate>0:
          #sumMothAge+=(#cbdate-#wbdate)/365.25
          #numMothAge+=1
        endif
      endif
    EndRepeat
    
    ! time for progress
    #fractionDone=(#i+1)/@fams.count
    if #fractionDone>#nextFraction
      NotifyProgress #fractionDone
      #nextFraction+=#fractionStepSize
    endif
  EndRepeat
EndSub

This subroutine collects all data on ages from the choosen family records in the file. It is where most of the work of this script is done; the work is done by interaction with your data through GEDitCOM II's scripting objects and their properties.

The first section defines and initializes global variables to accumulate age information. The Repeat loop enumerates over all family records passed to this subroutine. The loop "gets" the next family record (here fams.#i means ith element of the fams list) and then reads family record information — namely the husband and wife objects and the marriage date. The marriage date, like all dates in this script, is read as a serial day number (SDN).

The next section reads the parents' birth dates (if available). If neither spouse has a birthdate (both SDNs are zero), no age calculations are possible for this family so the loop continues to the next family. If either has a birthdate, the loop proceeds. If marriage date is known, the age of each spouse when married is calculated and added to global variables.

The age at child birth section is similar. It "gets" of list of children in the family and then loops over each one. For each child, it looks for their birth date. If a birth date is found, the ages of each parent with a known birth date are added to global variables.

Scripts that process many records, especially in large files, might take while to finish. It is good practice to provide feedback of script progress to the user. Here the variable fractionStepSize defines how often to notify the user and nextFraction is when the next notification occurs. The progress section at the end of the loop calculates current fraction done and when needed, notifies user of progress using the NotifyProgress command. Note that Internal Scripts run much faster than other languages meaning these progress posting might not even be visible. They will appear in very large files. Also note that the "continue" command if neither spouse has a birthdate means the progress check will not occur for that loop iteration. This issue could be resolved by reprogramming, but would not cause any problems (at worst, the progress indicator would not update enough).

When this subroutine is done, the global variables (e.g., #numHusbAge, #sumHusbAge, etc.) will contain all data needed to output the report. The subroutine ends and returns control to the main script. The next section explains formatting of the output report.


WriteToReport() Subroutine

Listing 3
! Write the Report and open in a window
Sub WriteToReport
  CreateScriptOutput rpt,#scriptName,"html"
  
  ! main title
  rpt.out "<h1>Generational Age Analysis in "&@gdoc.name&"</h1>"&return

  ! start table and give it a caption
  rpt.out "<table>"&return&"<caption>"&return
  rpt.out "Summary of spouse ages when married and when children were born"&return
  rpt.out "</caption>"&return
  
  ! column labels in the  section
  rpt.out "<thead><tr>"&return
  rpt.out "<th>Age Item</th><th>Husband</th><th>Wife</th>"&return
  rpt.out "</tr></thead>"&return
  
  ! the rows are in the tbody element
  rpt.out "<tbody>"&return
  
  ! rows for ages when married and when children were borm
  GoSub InsertRow "Avg. Age at Marriage",#numHusbAge,#sumHusbAge,#numWifeAge,#sumWifeAge
  GoSub InsertRow "Avg. Age at Childbirth",#numFathAge,#sumFathAge,#numMothAge,#sumMothAge

  ! end the tbody and table elements
  rpt.out "</tbody>"&return&"</table>"
  
  ! open in report window for this document
  rpt.write gdoc
EndSub

! Insert a row into the table
Sub InsertRow #rowLabel,#numHusb,#sumHusb,#numWife,#sumWife
  rpt.out "<tr><td>"&#rowLabel&"</td><td align='"
  if #numHusb>0
    rpt.out "right'>"&round((#sumHusb/#numHusb)&" 2")
  else
    rpt.out "center'>-"
  endif
  rpt.out "</td><td align='"
  if #numWife>0
    rpt.out "right'>"&round((#sumWife/#numWife)&" 2")
  else
    rpt.out "center'>-"
  endif
  rpt.out "</td></tr>"&return
EndSub

A good way to format reports in scripts is to use html elements. The process is made easier by a custom ScriptOutput object defined for Internal Scripts (and documented in the GEDitCOM Editor help). This subroutine starts by creating a ScriptOutput object in rpt (an example of a "Create" command initializing a scripting object). This subroutine then mainly use rpt.out object commands to add text to the report.

This report creates a small table using standard html elements for a table. The table begins with a caption and a header row. The InsertRow() subroutine is called twice to insert two rows for age at marriage and age at child birth. When inserting numbers, a round() expression in the Internal Scripting language is used to round the results to two digits.

When the subroutine is done, the rpt.write object command creates a report record and displays it in a GEDitCOM II window. To see the results of this subroutine, run the "Internal Script Custom Report" option of this extension. When the report is done, use the "View HTML Source" menu command to see the html content. The content added in this subroutine is enclosed within a div element.