Skip to content

Instantly share code, notes, and snippets.

@MikulasZelinka
Created December 21, 2023 21:13
Show Gist options
  • Save MikulasZelinka/e10132407ed2d8766b4621274aafec1b to your computer and use it in GitHub Desktop.
Save MikulasZelinka/e10132407ed2d8766b4621274aafec1b to your computer and use it in GitHub Desktop.
Sort a Google Doc by Headings (sections) while preserving contents (paragraphs) of each section
function onOpen() {
var ui = DocumentApp.getUi();
// Create custom menu
ui.createMenu('Sort and Write')
.addItem('Sort and Write Ascending', 'sortAndWriteAscending')
.addItem('Sort and Write Descending', 'sortAndWriteDescending')
.addToUi();
}
function sortAndWriteAscending() {
sortAndWrite(true);
}
function sortAndWriteDescending() {
sortAndWrite(false);
}
function sortAndWrite(ascending=true) {
var heading2List = extractHeadingStructure();
var entries = convertToEntryStructure(heading2List);
var sortedEntries = sortEntries(entries, ascending);
writeEntriesToDoc(sortedEntries)
}
// Function to extract Heading structure and paragraphs into a list of objects
function extractHeadingStructure() {
var doc = DocumentApp.getActiveDocument();
var body = doc.getBody();
// Create a list to store Heading 2 objects
var heading2List = [];
var paragraphs = body.getParagraphs();
var currentHeading2 = null;
var currentHeading2Object = null;
var currentHeading3 = null;
function parseHeading2Date(text) {
// Extract the year (YYYY) from Heading 2 text
var match = text.match(/\b\d{4}\b/);
var result = match ? parseInt(match[0]) : null
if (result) {
return result;
}
Logger.log("ERROR parsing year: " + text + " into date");
}
function parseHeading3Date(text) {
// Extract the sortable date (DD.MM.) from Heading 3 text
var match = text.match(/\b(\d{1,2})[^\d]*(\d{1,2})\b/);
if (match) {
var day = match[1].padStart(2, '0');
var month = match[2].padStart(2, '0');
return day + '.' + month + '.';
}
Logger.log("ERROR parsing day.month: " + text + " into date");
return null;
}
paragraphs.forEach(function (paragraph) {
var textStyle = paragraph.getAttributes();
var style = textStyle[DocumentApp.Attribute.HEADING];
// Skip empty headings
if (style && ((style === DocumentApp.ParagraphHeading.HEADING2) || (style === DocumentApp.ParagraphHeading.HEADING3)) && paragraph.getText().trim() === '') {
return;
}
if (style === DocumentApp.ParagraphHeading.HEADING2) {
// Handle Heading 2
currentHeading2 = paragraph.getText();
currentHeading3 = null;
currentHeading2Object = { heading2: currentHeading2, heading3List: [], date: parseHeading2Date(currentHeading2) };
heading2List.push(currentHeading2Object);
} else if (style === DocumentApp.ParagraphHeading.HEADING3) {
// Handle Heading 3
currentHeading3 = { heading3: paragraph.getText(), paragraphs: [], date: parseHeading3Date(paragraph.getText()) };
if (currentHeading2Object) {
currentHeading2Object.heading3List.push(currentHeading3);
}
} else {
// Handle regular paragraph text
if (currentHeading3) {
// If there is a Heading 3, append to its list of paragraphs
currentHeading3.paragraphs.push(paragraph.copy());
}
}
});
Logger.log('Heading 2 List:');
Logger.log(JSON.stringify(heading2List, null, 2));
// Return the extracted list of objects
return heading2List;
}
// Function to convert Heading 2, Heading 3 structure to Entry structure
function convertToEntryStructure(heading2List) {
var entryList = [];
heading2List.forEach(function (heading2Obj) {
var heading2 = heading2Obj.heading2;
var heading3List = heading2Obj.heading3List;
heading3List.forEach(function (heading3Obj) {
var heading3 = heading3Obj.heading3;
var date = heading3Obj.date;
var paragraphs = heading3Obj.paragraphs;
// Extract day and month from the date
var match = date.match(/(\d{1,2})\.(\d{1,2})\./);
var day = match ? match[1] : '';
var month = match ? match[2] : '';
// Create an Entry object
var entry = {
year: heading2,
month: month,
day: day,
title: "",
text: paragraphs
};
entryList.push(entry);
});
});
Logger.log('Entry List:');
Logger.log(JSON.stringify(entryList, null, 2));
// Return the converted list of Entry objects
return entryList;
}
// Function to sort entries based on date in ascending or descending order
function sortEntries(entryList, ascending) {
// Convert year, month, and day to integers for comparison
entryList.forEach(function (entry) {
entry.year = parseInt(entry.year);
entry.month = parseInt(entry.month);
entry.day = parseInt(entry.day);
});
// Default to ascending order if the sort direction is not provided or invalid
var sortDirection = ascending ? 1 : -1;
// Custom sorting function based on year, month, and day
function customSort(a, b) {
// Compare by year first, then month, and finally day
if (a.year !== b.year) {
return sortDirection * (a.year - b.year);
} else if (a.month !== b.month) {
return sortDirection * (a.month - b.month);
} else {
return sortDirection * (a.day - b.day);
}
}
// Sort the entryList using the custom sort function
entryList.sort(customSort);
// Log the sorted entryList
Logger.log('Sorted Entry List:');
Logger.log(JSON.stringify(entryList, null, 2));
// Return the sorted entryList
return entryList;
}
// Function to create a new Google Doc and write entries in the specified order
function writeEntriesToDoc(sortedEntryList) {
var doc = DocumentApp.getActiveDocument();
var currentDocName = doc.getName();
var newDoc = DocumentApp.create(currentDocName + '_sorted');
var newDocBody = newDoc.getBody();
// Czech month names
var czechMonths = [
"Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec",
"Srpen", "Září", "Říjen", "Listopad", "Prosinec"
];
var czechMonthsGenitiv = [
"ledna", "února", "března", "dubna", "května", "června", "července",
"srpna", "září", "října", "listopadu", "prosince"
];
// Initialize variables to keep track of current year, month, and day
var currentYear = null;
var currentMonth = null;
var currentDay = null;
// Iterate through the sorted entries and write to the new document
sortedEntryList.forEach(function (entry) {
// Check if the year has changed
if (entry.year !== currentYear) {
newDocBody.appendParagraph(String(entry.year)).setHeading(DocumentApp.ParagraphHeading.HEADING2);
currentYear = entry.year;
// Reset month and day when year changes
currentMonth = null;
currentDay = null;
}
// Check if the month has changed
if (entry.month !== currentMonth) {
newDocBody.appendParagraph(`${czechMonths[entry.month - 1]} ${entry.year}`).setHeading(DocumentApp.ParagraphHeading.HEADING3);
currentMonth = entry.month;
// Reset day when month changes
currentDay = null;
}
// Check if the day has changed
if (entry.day !== currentDay) {
// var formattedDate = Utilities.formatString('%02d. %02d. %d', entry.day, entry.month, entry.year);
var formattedDate = `${entry.day}. ${czechMonthsGenitiv[entry.month - 1]} ${entry.year}`;
newDocBody.appendParagraph(formattedDate).setHeading(DocumentApp.ParagraphHeading.HEADING4);
currentDay = entry.day;
}
// Append paragraphs as regular text, images as image, etc.
entry.text.forEach(function (paragraph) {
try {
newDocBody.appendParagraph(paragraph);
} catch {
try {
newDocBody.appendListItem(paragraph);
} catch {
try {
newDocBody.appendImage(paragraph);
} catch {
try {
newDocBody.appendPageBreak(paragraph);
} catch {
try {
newDocBody.appendTable(paragraph);
} catch (e) {
throw e;
}
}
}
}
}
});
});
// Log the URL of the new document
Logger.log('New document created and entries written successfully.');
Logger.log('New document URL: ' + newDoc.getUrl());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment