From leamhall at gmail.com Sat May 4 11:15:13 2024 From: leamhall at gmail.com (Leam Hall) Date: Sat, 4 May 2024 10:15:13 -0500 Subject: [Tutor] Ideas on making this cleaner? Message-ID: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> This gets a rough USA grade level report for chapters in a book. There's a lot of "count()" duplication, can it be done in a cleaner fashion? Coding style is Python 3.12 and standard library only; no exceptions. I also run "black -l 79" on everything. :) Full code is at: https://github.com/LeamHall/bookbot class Report: def __init__(self, data, filename): self.filename = filename self.lines = list() for line in data: self.lines.append(line.lower()) self.count_sentences() self.count_words() self.count_syallables() self.grade_report() def count_sentences(self): """Counts the number of sentence ending marks.""" self.sentence_count = 0 for line in self.lines: self.sentence_count += line.count(".") self.sentence_count += line.count("?") self.sentence_count += line.count("!") def count_words(self): """Counts the number of words, ignoring punctuation.""" self.word_count = 0 for line in self.lines: self.word_count += len(line.split()) def count_syallables(self): """Simplistic syllable counter. Does not handle unicode.""" self.syllable_count = 0 for line in self.lines: self.syllable_count += line.count("a") self.syllable_count += line.count("e") self.syllable_count += line.count("i") self.syllable_count += line.count("o") self.syllable_count += line.count("u") self.syllable_count -= line.count("ee") self.syllable_count -= line.count("oi") self.syllable_count -= line.count("oo") self.syllable_count -= line.count("ou") scrubbed_line = line.replace(".", " ") scrubbed_line = scrubbed_line.replace("!", " ") scrubbed_line = scrubbed_line.replace("?", " ") scrubbed_line = scrubbed_line.replace('"', " ") words = scrubbed_line.split() for word in words: for phrase in ["e", "ey"]: if word.endswith(phrase): self.syllable_count -= 1 for phrase in [ "y", ]: if word.endswith(phrase): self.syllable_count += 1 if self.syllable_count < 1: self.syllable_count = 1 def grade_report(self): """Calculates grade level per: https://en.wikipedia.org/wiki/Flesch%E2%80%93Kincaid_readability_tests """ self.sentence_average = self.word_count / self.sentence_count self.syllables_per_word_average = self.syllable_count / self.word_count self.grade_level = ( (0.39 * self.sentence_average) + (11.8 * self.syllables_per_word_average) - 15.59 ) self.grade_level = float("{:.2f}".format(self.grade_level)) def report_data(self): """Collates and returns report data.""" data = dict() data["filename"] = self.filename data["sentence_average"] = self.sentence_average data["grade_level"] = self.grade_level data["syllables_per_word_average"] = self.syllables_per_word_average return data -- Software Engineer (reuel.net/resume) Scribe: The Domici War (domiciwar.net) General Ne'er-do-well (github.com/LeamHall) From threesomequarks at proton.me Sat May 4 13:32:27 2024 From: threesomequarks at proton.me (ThreeBlindQuarks) Date: Sat, 04 May 2024 17:32:27 +0000 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> References: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> Message-ID: Leam, I some cases, a dictionary is a useful method to consolidate multiple cases. For example, your vowels: > self.syllable_count += line.count("a") > self.syllable_count += line.count("e") > self.syllable_count += line.count("i") > self.syllable_count += line.count("o") > self.syllable_count += line.count("u") > self.syllable_count -= line.count("ee") > self.syllable_count -= line.count("oi") > self.syllable_count -= line.count("oo") > self.syllable_count -= line.count("ou") You could initialize a dictionary like Vowels with keys like "a" through "ou" initialized to zero and increment it when a vowel is encountered. This is not always trivial or even helpful as you need to deal with making sure you only include the vowels you want and still have to iterate over things. Sent with Proton Mail secure email. On Saturday, May 4th, 2024 at 11:15 AM, Leam Hall wrote: > This gets a rough USA grade level report for chapters in a book. There's a lot of "count()" duplication, can it be done in a cleaner fashion? Coding style is Python 3.12 and standard library only; no exceptions. I also run "black -l 79" on everything. :) > > Full code is at: https://github.com/LeamHall/bookbot > > > class Report: > def init(self, data, filename): > self.filename = filename > self.lines = list() > for line in data: > self.lines.append(line.lower()) > self.count_sentences() > self.count_words() > self.count_syallables() > self.grade_report() > > def count_sentences(self): > """Counts the number of sentence ending marks.""" > self.sentence_count = 0 > for line in self.lines: > self.sentence_count += line.count(".") > self.sentence_count += line.count("?") > self.sentence_count += line.count("!") > > def count_words(self): > """Counts the number of words, ignoring punctuation.""" > self.word_count = 0 > for line in self.lines: > self.word_count += len(line.split()) > > def count_syallables(self): > """Simplistic syllable counter. Does not handle unicode.""" > self.syllable_count = 0 > for line in self.lines: > self.syllable_count += line.count("a") > self.syllable_count += line.count("e") > self.syllable_count += line.count("i") > self.syllable_count += line.count("o") > self.syllable_count += line.count("u") > self.syllable_count -= line.count("ee") > self.syllable_count -= line.count("oi") > self.syllable_count -= line.count("oo") > self.syllable_count -= line.count("ou") > scrubbed_line = line.replace(".", " ") > scrubbed_line = scrubbed_line.replace("!", " ") > scrubbed_line = scrubbed_line.replace("?", " ") > scrubbed_line = scrubbed_line.replace('"', " ") > words = scrubbed_line.split() > for word in words: > for phrase in ["e", "ey"]: > if word.endswith(phrase): > self.syllable_count -= 1 > for phrase in [ > "y", > ]: > if word.endswith(phrase): > self.syllable_count += 1 > if self.syllable_count < 1: > self.syllable_count = 1 > > def grade_report(self): > """Calculates grade level per: > https://en.wikipedia.org/wiki/Flesch?Kincaid_readability_tests > """ > self.sentence_average = self.word_count / self.sentence_count > self.syllables_per_word_average = self.syllable_count / self.word_count > self.grade_level = ( > (0.39 * self.sentence_average) > + (11.8 * self.syllables_per_word_average) > - 15.59 > ) > self.grade_level = float("{:.2f}".format(self.grade_level)) > > def report_data(self): > """Collates and returns report data.""" > data = dict() > data["filename"] = self.filename > data["sentence_average"] = self.sentence_average > data["grade_level"] = self.grade_level > data["syllables_per_word_average"] = self.syllables_per_word_average > return data > > > -- > Software Engineer (reuel.net/resume) > Scribe: The Domici War (domiciwar.net) > General Ne'er-do-well (github.com/LeamHall) > _______________________________________________ > Tutor maillist - Tutor at python.org > To unsubscribe or change subscription options: > https://mail.python.org/mailman/listinfo/tutor From mats at wichmann.us Sat May 4 13:51:12 2024 From: mats at wichmann.us (Mats Wichmann) Date: Sat, 4 May 2024 11:51:12 -0600 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: References: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> Message-ID: On 5/4/24 11:32, ThreeBlindQuarks via Tutor wrote: > > > Leam, > > I some cases, a dictionary is a useful method to consolidate multiple cases. > > For example, your vowels: >> self.syllable_count += line.count("a") >> self.syllable_count += line.count("e") >> self.syllable_count += line.count("i") >> self.syllable_count += line.count("o") >> self.syllable_count += line.count("u") >> self.syllable_count -= line.count("ee") >> self.syllable_count -= line.count("oi") >> self.syllable_count -= line.count("oo") >> self.syllable_count -= line.count("ou") > > You could initialize a dictionary like Vowels with keys like "a" through "ou" initialized to zero and increment it when a vowel is encountered. In fact, the stdlib Counter class can be helpful in such usage: https://docs.python.org/3/library/collections.html#counter-objects From threesomequarks at proton.me Sat May 4 14:01:02 2024 From: threesomequarks at proton.me (ThreeBlindQuarks) Date: Sat, 04 May 2024 18:01:02 +0000 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: References: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> Message-ID: I looked more carefully at the code and noticed something I am not sure is like what I have seen before in how one uses an object. None of the methods seems to be designed to be called from outside the class initialization. I see a dunder init that simply invokes all the other methods and some of those methods invoke each other. After initialization, you have an object with instance variables just sitting there. This is not in any way illegal, but probably could have been done many other ways such as adding a calculate_now method and not doing it all on initialization, or having the methods used be nested and defined only in the initialization method. I am curious how this class/object is being used. Sent with Proton Mail secure email. On Saturday, May 4th, 2024 at 1:32 PM, ThreeBlindQuarks via Tutor wrote: > > Leam, > > I some cases, a dictionary is a useful method to consolidate multiple cases. > > For example, your vowels: > > > self.syllable_count += line.count("a") > > self.syllable_count += line.count("e") > > self.syllable_count += line.count("i") > > self.syllable_count += line.count("o") > > self.syllable_count += line.count("u") > > self.syllable_count -= line.count("ee") > > self.syllable_count -= line.count("oi") > > self.syllable_count -= line.count("oo") > > self.syllable_count -= line.count("ou") > > > You could initialize a dictionary like Vowels with keys like "a" through "ou" initialized to zero and increment it when a vowel is encountered. > > This is not always trivial or even helpful as you need to deal with making sure you only include the vowels you want and still have to iterate over things. > > > Sent with Proton Mail secure email. > > > On Saturday, May 4th, 2024 at 11:15 AM, Leam Hall leamhall at gmail.com wrote: > > > This gets a rough USA grade level report for chapters in a book. There's a lot of "count()" duplication, can it be done in a cleaner fashion? Coding style is Python 3.12 and standard library only; no exceptions. I also run "black -l 79" on everything. :) > > > > Full code is at: https://github.com/LeamHall/bookbot > > > > class Report: > > def init(self, data, filename): > > self.filename = filename > > self.lines = list() > > for line in data: > > self.lines.append(line.lower()) > > self.count_sentences() > > self.count_words() > > self.count_syallables() > > self.grade_report() > > > > def count_sentences(self): > > """Counts the number of sentence ending marks.""" > > self.sentence_count = 0 > > for line in self.lines: > > self.sentence_count += line.count(".") > > self.sentence_count += line.count("?") > > self.sentence_count += line.count("!") > > > > def count_words(self): > > """Counts the number of words, ignoring punctuation.""" > > self.word_count = 0 > > for line in self.lines: > > self.word_count += len(line.split()) > > > > def count_syallables(self): > > """Simplistic syllable counter. Does not handle unicode.""" > > self.syllable_count = 0 > > for line in self.lines: > > self.syllable_count += line.count("a") > > self.syllable_count += line.count("e") > > self.syllable_count += line.count("i") > > self.syllable_count += line.count("o") > > self.syllable_count += line.count("u") > > self.syllable_count -= line.count("ee") > > self.syllable_count -= line.count("oi") > > self.syllable_count -= line.count("oo") > > self.syllable_count -= line.count("ou") > > scrubbed_line = line.replace(".", " ") > > scrubbed_line = scrubbed_line.replace("!", " ") > > scrubbed_line = scrubbed_line.replace("?", " ") > > scrubbed_line = scrubbed_line.replace('"', " ") > > words = scrubbed_line.split() > > for word in words: > > for phrase in ["e", "ey"]: > > if word.endswith(phrase): > > self.syllable_count -= 1 > > for phrase in [ > > "y", > > ]: > > if word.endswith(phrase): > > self.syllable_count += 1 > > if self.syllable_count < 1: > > self.syllable_count = 1 > > > > def grade_report(self): > > """Calculates grade level per: > > https://en.wikipedia.org/wiki/Flesch?Kincaid_readability_tests > > """ > > self.sentence_average = self.word_count / self.sentence_count > > self.syllables_per_word_average = self.syllable_count / self.word_count > > self.grade_level = ( > > (0.39 * self.sentence_average) > > + (11.8 * self.syllables_per_word_average) > > - 15.59 > > ) > > self.grade_level = float("{:.2f}".format(self.grade_level)) > > > > def report_data(self): > > """Collates and returns report data.""" > > data = dict() > > data["filename"] = self.filename > > data["sentence_average"] = self.sentence_average > > data["grade_level"] = self.grade_level > > data["syllables_per_word_average"] = self.syllables_per_word_average > > return data > > > > -- > > Software Engineer (reuel.net/resume) > > Scribe: The Domici War (domiciwar.net) > > General Ne'er-do-well (github.com/LeamHall) > > _______________________________________________ > > Tutor maillist - Tutor at python.org > > To unsubscribe or change subscription options: > > https://mail.python.org/mailman/listinfo/tutor > > _______________________________________________ > Tutor maillist - Tutor at python.org > To unsubscribe or change subscription options: > https://mail.python.org/mailman/listinfo/tutor From leamhall at gmail.com Sat May 4 15:44:09 2024 From: leamhall at gmail.com (Leam Hall) Date: Sat, 4 May 2024 14:44:09 -0500 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: References: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> Message-ID: <645c086b-9895-4767-d6d2-f535824aa714@gmail.com> Before I answer that, I just wanted to ask; is my email keeping the indentation properly? It looks like it on the Tutor archive, but I'm never quite sure how other's mail handles this. TBQ, the Report class is a builder inside the Chapter class. You're right in that most (all?) of the Report methods could be prefixed with an underscore to mark them as intended to be private, their only use is to create the data bits for the report_data dictionary. The report data gets called by they Chapter class, and is later written to a report file. Does that answer the question, or did I misunderstand? Leam On 5/4/24 13:01, ThreeBlindQuarks wrote: > > I looked more carefully at the code and noticed something I am not sure is like what I have seen before in how one uses an object. > > None of the methods seems to be designed to be called from outside the class initialization. I see a dunder init that simply invokes all the other methods and some of those methods invoke each other. After initialization, you have an object with instance variables just sitting there. > > This is not in any way illegal, but probably could have been done many other ways such as adding a calculate_now method and not doing it all on initialization, or having the methods used be nested and defined only in the initialization method. > > I am curious how this class/object is being used. > > > > > Sent with Proton Mail secure email. > > On Saturday, May 4th, 2024 at 1:32 PM, ThreeBlindQuarks via Tutor wrote: > >> >> Leam, >> >> I some cases, a dictionary is a useful method to consolidate multiple cases. >> >> For example, your vowels: >> >>> self.syllable_count += line.count("a") >>> self.syllable_count += line.count("e") >>> self.syllable_count += line.count("i") >>> self.syllable_count += line.count("o") >>> self.syllable_count += line.count("u") >>> self.syllable_count -= line.count("ee") >>> self.syllable_count -= line.count("oi") >>> self.syllable_count -= line.count("oo") >>> self.syllable_count -= line.count("ou") >> >> >> You could initialize a dictionary like Vowels with keys like "a" through "ou" initialized to zero and increment it when a vowel is encountered. >> >> This is not always trivial or even helpful as you need to deal with making sure you only include the vowels you want and still have to iterate over things. >> >> >> Sent with Proton Mail secure email. >> >> >> On Saturday, May 4th, 2024 at 11:15 AM, Leam Hall leamhall at gmail.com wrote: >> >>> This gets a rough USA grade level report for chapters in a book. There's a lot of "count()" duplication, can it be done in a cleaner fashion? Coding style is Python 3.12 and standard library only; no exceptions. I also run "black -l 79" on everything. :) >>> >>> Full code is at: https://github.com/LeamHall/bookbot >>> >>> class Report: >>> def init(self, data, filename): >>> self.filename = filename >>> self.lines = list() >>> for line in data: >>> self.lines.append(line.lower()) >>> self.count_sentences() >>> self.count_words() >>> self.count_syallables() >>> self.grade_report() >>> >>> def count_sentences(self): >>> """Counts the number of sentence ending marks.""" >>> self.sentence_count = 0 >>> for line in self.lines: >>> self.sentence_count += line.count(".") >>> self.sentence_count += line.count("?") >>> self.sentence_count += line.count("!") >>> >>> def count_words(self): >>> """Counts the number of words, ignoring punctuation.""" >>> self.word_count = 0 >>> for line in self.lines: >>> self.word_count += len(line.split()) >>> >>> def count_syallables(self): >>> """Simplistic syllable counter. Does not handle unicode.""" >>> self.syllable_count = 0 >>> for line in self.lines: >>> self.syllable_count += line.count("a") >>> self.syllable_count += line.count("e") >>> self.syllable_count += line.count("i") >>> self.syllable_count += line.count("o") >>> self.syllable_count += line.count("u") >>> self.syllable_count -= line.count("ee") >>> self.syllable_count -= line.count("oi") >>> self.syllable_count -= line.count("oo") >>> self.syllable_count -= line.count("ou") >>> scrubbed_line = line.replace(".", " ") >>> scrubbed_line = scrubbed_line.replace("!", " ") >>> scrubbed_line = scrubbed_line.replace("?", " ") >>> scrubbed_line = scrubbed_line.replace('"', " ") >>> words = scrubbed_line.split() >>> for word in words: >>> for phrase in ["e", "ey"]: >>> if word.endswith(phrase): >>> self.syllable_count -= 1 >>> for phrase in [ >>> "y", >>> ]: >>> if word.endswith(phrase): >>> self.syllable_count += 1 >>> if self.syllable_count < 1: >>> self.syllable_count = 1 >>> >>> def grade_report(self): >>> """Calculates grade level per: >>> https://en.wikipedia.org/wiki/Flesch?Kincaid_readability_tests >>> """ >>> self.sentence_average = self.word_count / self.sentence_count >>> self.syllables_per_word_average = self.syllable_count / self.word_count >>> self.grade_level = ( >>> (0.39 * self.sentence_average) >>> + (11.8 * self.syllables_per_word_average) >>> - 15.59 >>> ) >>> self.grade_level = float("{:.2f}".format(self.grade_level)) >>> >>> def report_data(self): >>> """Collates and returns report data.""" >>> data = dict() >>> data["filename"] = self.filename >>> data["sentence_average"] = self.sentence_average >>> data["grade_level"] = self.grade_level >>> data["syllables_per_word_average"] = self.syllables_per_word_average >>> return data >>> >>> -- >>> Software Engineer (reuel.net/resume) >>> Scribe: The Domici War (domiciwar.net) >>> General Ne'er-do-well (github.com/LeamHall) >>> _______________________________________________ >>> Tutor maillist - Tutor at python.org >>> To unsubscribe or change subscription options: >>> https://mail.python.org/mailman/listinfo/tutor >> >> _______________________________________________ >> Tutor maillist - Tutor at python.org >> To unsubscribe or change subscription options: >> https://mail.python.org/mailman/listinfo/tutor -- Software Engineer (reuel.net/resume) Scribe: The Domici War (domiciwar.net) General Ne'er-do-well (github.com/LeamHall) From cs at cskk.id.au Sat May 4 17:46:02 2024 From: cs at cskk.id.au (Cameron Simpson) Date: Sun, 5 May 2024 07:46:02 +1000 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: <645c086b-9895-4767-d6d2-f535824aa714@gmail.com> References: <645c086b-9895-4767-d6d2-f535824aa714@gmail.com> Message-ID: On 04May2024 14:44, Leam Hall wrote: >Before I answer that, I just wanted to ask; is my email keeping the indentation properly? The first post looked just fine to me. From threesomequarks at proton.me Sat May 4 18:25:49 2024 From: threesomequarks at proton.me (ThreeBlindQuarks) Date: Sat, 04 May 2024 22:25:49 +0000 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: <645c086b-9895-4767-d6d2-f535824aa714@gmail.com> References: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> <645c086b-9895-4767-d6d2-f535824aa714@gmail.com> Message-ID: Leam, The initial message was indented fine for me albeit some of the replies ended up flush to the left. My question was more about the design parameters. Generally, when a class DOES something, there is a way to access it or why bother? So, in your code, you start with __init__() and do not declare any variables in advance. the initializer does create variables like "filename" and "lines" and various of the other methods create others like "sentence_count" and "word_count". When the initializer has completed, all the variables presumably are set and you could ask for a new object called myreport to share info such as myreport.filename or myreport.word_count. On my first quick read, I did not notice there was indeed a method that was not invoked by the initializer and that the intent was to be able to ask for myreport.report_data which creates a temporary dictionary in the namespace of the function, and returns it, but not in the namespace of the object. So, I see your design now. Again, not objecting, just wrapping my mind around how you chose to do it. If a report is only asked for once, or maybe never, this is fine. Other designs might make the report automatically at startup or delay all the processing until and unless a report was requested or even keep the report in the class when asked for the first time, and a subsequent request would just return the previously computed values. In such a variant, you might even remove the variables already in the dictionary created to save space. There are MANY choices and none need be right or wrong and it may depend on how you see you or others using this class. My other comment may need explaining. I was not so much worried about using an initial underscore to make some methods look private, albeit some purists might. What I was wondering about is whether the design would be faster or simpler if you had only two methods in the first place. In Python, you can define a function within the body of an existing function and use it and other internal functions fairly easily. Are there advantages and disadvantages? Do these functions need to be passed and use "self" and will they run any slower or faster than the way you chose? I am simply saying that as a design, I might have tried to use internal functions rather than methods never meant to be invoked from outside. Your main question was whether you could do some of this in other ways and it is in that spirit I mentioned it. We probably could discuss things you did not ask about. For example, your many functions each traverse the same lines. But they all scribble on the same namespace rather than return a result to the called in the initializer. Thus, could you have traversed the list of lines once and done each step in the same place, either in one or a few methods or the main initializer method? I mentioned using dictionaries in some places and another one to consider is using sets and testing set membership which can look superficially simpler than listing choices. And, in a sense, it could generalize better should you ever decide to adapt your code to understand reports in another language where you might have alternate sets of vowels and so on. Sent with Proton Mail secure email. On Saturday, May 4th, 2024 at 3:44 PM, Leam Hall wrote: > Before I answer that, I just wanted to ask; is my email keeping the indentation properly? It looks like it on the Tutor archive, but I'm never quite sure how other's mail handles this. > > TBQ, the Report class is a builder inside the Chapter class. You're right in that most (all?) of the Report methods could be prefixed with an underscore to mark them as intended to be private, their only use is to create the data bits for the report_data dictionary. The report data gets called by they Chapter class, and is later written to a report file. > > Does that answer the question, or did I misunderstand? > > Leam > > > > On 5/4/24 13:01, ThreeBlindQuarks wrote: > > > I looked more carefully at the code and noticed something I am not sure is like what I have seen before in how one uses an object. > > > > None of the methods seems to be designed to be called from outside the class initialization. I see a dunder init that simply invokes all the other methods and some of those methods invoke each other. After initialization, you have an object with instance variables just sitting there. > > > > This is not in any way illegal, but probably could have been done many other ways such as adding a calculate_now method and not doing it all on initialization, or having the methods used be nested and defined only in the initialization method. > > > > I am curious how this class/object is being used. > > > > Sent with Proton Mail secure email. > > > > On Saturday, May 4th, 2024 at 1:32 PM, ThreeBlindQuarks via Tutor tutor at python.org wrote: > > > > > Leam, > > > > > > I some cases, a dictionary is a useful method to consolidate multiple cases. > > > > > > For example, your vowels: > > > > > > > self.syllable_count += line.count("a") > > > > self.syllable_count += line.count("e") > > > > self.syllable_count += line.count("i") > > > > self.syllable_count += line.count("o") > > > > self.syllable_count += line.count("u") > > > > self.syllable_count -= line.count("ee") > > > > self.syllable_count -= line.count("oi") > > > > self.syllable_count -= line.count("oo") > > > > self.syllable_count -= line.count("ou") > > > > > > You could initialize a dictionary like Vowels with keys like "a" through "ou" initialized to zero and increment it when a vowel is encountered. > > > > > > This is not always trivial or even helpful as you need to deal with making sure you only include the vowels you want and still have to iterate over things. > > > > > > Sent with Proton Mail secure email. > > > > > > On Saturday, May 4th, 2024 at 11:15 AM, Leam Hall leamhall at gmail.com wrote: > > > > > > > This gets a rough USA grade level report for chapters in a book. There's a lot of "count()" duplication, can it be done in a cleaner fashion? Coding style is Python 3.12 and standard library only; no exceptions. I also run "black -l 79" on everything. :) > > > > > > > > Full code is at: https://github.com/LeamHall/bookbot > > > > > > > > class Report: > > > > def init(self, data, filename): > > > > self.filename = filename > > > > self.lines = list() > > > > for line in data: > > > > self.lines.append(line.lower()) > > > > self.count_sentences() > > > > self.count_words() > > > > self.count_syallables() > > > > self.grade_report() > > > > > > > > def count_sentences(self): > > > > """Counts the number of sentence ending marks.""" > > > > self.sentence_count = 0 > > > > for line in self.lines: > > > > self.sentence_count += line.count(".") > > > > self.sentence_count += line.count("?") > > > > self.sentence_count += line.count("!") > > > > > > > > def count_words(self): > > > > """Counts the number of words, ignoring punctuation.""" > > > > self.word_count = 0 > > > > for line in self.lines: > > > > self.word_count += len(line.split()) > > > > > > > > def count_syallables(self): > > > > """Simplistic syllable counter. Does not handle unicode.""" > > > > self.syllable_count = 0 > > > > for line in self.lines: > > > > self.syllable_count += line.count("a") > > > > self.syllable_count += line.count("e") > > > > self.syllable_count += line.count("i") > > > > self.syllable_count += line.count("o") > > > > self.syllable_count += line.count("u") > > > > self.syllable_count -= line.count("ee") > > > > self.syllable_count -= line.count("oi") > > > > self.syllable_count -= line.count("oo") > > > > self.syllable_count -= line.count("ou") > > > > scrubbed_line = line.replace(".", " ") > > > > scrubbed_line = scrubbed_line.replace("!", " ") > > > > scrubbed_line = scrubbed_line.replace("?", " ") > > > > scrubbed_line = scrubbed_line.replace('"', " ") > > > > words = scrubbed_line.split() > > > > for word in words: > > > > for phrase in ["e", "ey"]: > > > > if word.endswith(phrase): > > > > self.syllable_count -= 1 > > > > for phrase in [ > > > > "y", > > > > ]: > > > > if word.endswith(phrase): > > > > self.syllable_count += 1 > > > > if self.syllable_count < 1: > > > > self.syllable_count = 1 > > > > > > > > def grade_report(self): > > > > """Calculates grade level per: > > > > https://en.wikipedia.org/wiki/Flesch?Kincaid_readability_tests > > > > """ > > > > self.sentence_average = self.word_count / self.sentence_count > > > > self.syllables_per_word_average = self.syllable_count / self.word_count > > > > self.grade_level = ( > > > > (0.39 * self.sentence_average) > > > > + (11.8 * self.syllables_per_word_average) > > > > - 15.59 > > > > ) > > > > self.grade_level = float("{:.2f}".format(self.grade_level)) > > > > > > > > def report_data(self): > > > > """Collates and returns report data.""" > > > > data = dict() > > > > data["filename"] = self.filename > > > > data["sentence_average"] = self.sentence_average > > > > data["grade_level"] = self.grade_level > > > > data["syllables_per_word_average"] = self.syllables_per_word_average > > > > return data > > > > > > > > -- > > > > Software Engineer (reuel.net/resume) > > > > Scribe: The Domici War (domiciwar.net) > > > > General Ne'er-do-well (github.com/LeamHall) > > > > _______________________________________________ > > > > Tutor maillist - Tutor at python.org > > > > To unsubscribe or change subscription options: > > > > https://mail.python.org/mailman/listinfo/tutor > > > > > > _______________________________________________ > > > Tutor maillist - Tutor at python.org > > > To unsubscribe or change subscription options: > > > https://mail.python.org/mailman/listinfo/tutor > > > -- > Software Engineer (reuel.net/resume) > Scribe: The Domici War (domiciwar.net) > General Ne'er-do-well (github.com/LeamHall) > _______________________________________________ > Tutor maillist - Tutor at python.org > To unsubscribe or change subscription options: > https://mail.python.org/mailman/listinfo/tutor From alan.gauld at yahoo.co.uk Sat May 4 18:42:33 2024 From: alan.gauld at yahoo.co.uk (Alan Gauld) Date: Sat, 4 May 2024 23:42:33 +0100 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: <645c086b-9895-4767-d6d2-f535824aa714@gmail.com> References: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> <645c086b-9895-4767-d6d2-f535824aa714@gmail.com> Message-ID: On 04/05/2024 20:44, Leam Hall wrote: > TBQ, the Report class is a builder inside the Chapter class. I think the question is, why a class and not just a function? You could just have a report module with a report_data() function that takes the same inputs as the constructor. There is no real value in creating an instance of the class since it does not hold any useful state(the individual reports could be created on demand) The argument for retaining a class is if you subsequently compare different chapter reports but then there should be comparison methods in the class... So arguably you could clean up the code by removing the class and just making a module with a set of functions. BTW. For a slightly different solution to a very similar problem check out the grammar checker case study in my tutorial... It doesn't do the "level report" but it does count the various grammar parts. It uses regex for the punctuation/splitting etc > ...The report data gets called by they Chapter class, and > is later written to a report file. So where is the method for writing it to a file? eg: def store(self, filename=self.filename):... Finally, a tiny bit of clean up would be to use a list comp to populate lines: class Report: def __init__(self, data, filename): self.filename = filename self.lines = list() for line in data: self.lines.append(line.lower()) ... Becomes: class Report: def __init__(self, data, filename): self.filename = filename self.lines = [line.lower for line in data] ... -- Alan G Author of the Learn to Program web site http://www.alan-g.me.uk/ http://www.amazon.com/author/alan_gauld Follow my photo-blog on Flickr at: http://www.flickr.com/photos/alangauldphotos From PythonList at DancesWithMice.info Sun May 5 02:26:16 2024 From: PythonList at DancesWithMice.info (dn) Date: Sun, 5 May 2024 18:26:16 +1200 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: References: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> <645c086b-9895-4767-d6d2-f535824aa714@gmail.com> Message-ID: <479c11ce-6184-4ca2-a380-6dc586cf8564@DancesWithMice.info> Hi Leam, On 5/05/24 10:42, Alan Gauld via Tutor wrote: > On 04/05/2024 20:44, Leam Hall wrote: > >> TBQ, the Report class is a builder inside the Chapter class. > > I think the question is, why a class and not just a function? > You could just have a report module with a report_data() function > that takes the same inputs as the constructor. There is no real > value in creating an instance of the class since it does not > hold any useful state(the individual reports could be created > on demand) The argument for retaining a class is if you > subsequently compare different chapter reports but then > there should be comparison methods in the class... > > So arguably you could clean up the code by removing the > class and just making a module with a set of functions. Yes, even this class-y guy was thinking the same. Remember that a Python-module is also a self-contained name-space, and thus would give many of the advantages discussed. Noted inconsistent spelling of "syllable". Prefer not to have 'magic constants', but some might (quite reasonably) argue that: QUESTION_MARK = "?" is more an affectation than adding quality to the code-base. Found the word "grade" confusing/a mis-use. Usually it is referring to degrees of pass/fail, eg an examination; whereas this is about 'readability'. Perhaps a (class) docstring would have helped, but silly-boys (like...) would probably jump to the incorrect assumption anyway... -- Regards, =dn From leamhall at gmail.com Sun May 5 08:59:17 2024 From: leamhall at gmail.com (Leam Hall) Date: Sun, 5 May 2024 07:59:17 -0500 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: References: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> Message-ID: <58d8e3b7-c9f2-abd9-7372-de9a00dd81f7@gmail.com> Hey all, thanks for letting me know the message was formatted correctly! On 5/4/24 12:51, Mats Wichmann wrote: > In fact, the stdlib Counter class can be helpful in such usage: > > https://docs.python.org/3/library/collections.html#counter-objects Oh, nice! Reading up on Counter now. On 5/4/24 17:25, ThreeBlindQuarks wrote: > Generally, when a class DOES something, there is a way to access it or why bother? Yes, the Report object is composed into the Chapter object, with Chapter providing the end report result to a collation method elsewhere. The reasoning is fairly simple; objects are a collection of data and processes. In this case, the Reports data consists of the grade level math and the sequence of steps to derive the result. The processes support that, and end with a result that gets accesses by the Chapter object. I wanted to keep each Report with each Chapter, so all of the chapter meta data was in one place. > If a report is only asked for once, or maybe never, this is fine. Other designs might make the report automatically at startup or delay all the processing until and unless a report was requested or even keep the report in the class when asked for the first time, and a subsequent request would just return the previously computed values. In such a variant, you might even remove the variables already in the dictionary created to save space. Yup, it is collated once and printed to a file. I'm a tool builder more than an application developer, and all of the data and processes are wrapped up in one file that gets called maybe a dozen times for any one book. I don't object to higher performance, but it only takes a second or two to process an entire novel. Removing variables, or having one long method that only creates the variables in process seems to add complexity. I'm a simple minded person, "easy to understand" is my go-to. :) Here are the last few lines of the report file. Each chapter has it's own report data, and the last line is the average of all chapters. I intentionally write easy reading books so people who aren't good readers, or whose parent language isn't English, can enjoy the story without struggling. Filename: 1429_180_0745_Casimir_District_Saorsa.txt Grade Level: 2.3 Average Grade Level: 2.9 On 5/4/24 17:42, Alan Gauld via Tutor wrote: > I think the question is, why a class and not just a function? The earliest incarnation of this was a Perl based "one big thing". While moving to Python I wanted the encapsulation of a Class so I could work on each bit incrementally and worry less about "changing X breaks Y". I'm pretty good at breaking things, and use the Class to isolate the damage. > BTW. For a slightly different solution to a very similar problem > check out the grammar checker case study in my tutorial... I just made a note to do that, it's on my whiteboard right beside my head. :) > So where is the method for writing it to a file? eg: > > def store(self, filename=self.filename):... Outside of the report object itself. The writer collates all of the data, that lets it write for each chapter and also produce data on the entire book. > Becomes: > > class Report: > def __init__(self, data, filename): > self.filename = filename > self.lines = [line.lower for line in data] > ... I'm definitely stealing this! On 5/5/24 01:26, dn via Tutor wrote: >> So arguably you could clean up the code by removing the >> class and just making a module with a set of functions. > > Yes, even this class-y guy was thinking the same. > > Remember that a Python-module is also a self-contained name-space, and thus would give many of the advantages discussed. I'll probably upset folks for saying this, but I don't use third party modules or write code with non-stdlib libraries. I'm more of a tool builder; if someone has to go get other modules or even pip install mine then it destroys Python's portability. I want to give someone a bit of code and them not have to be a Pythonista or CompSci major. "Here's the code, it runs as is.". If third party stuff is required then I'd rather use a compiled language: "Here's the code, it runs as is." > Found the word "grade" confusing/a mis-use. Usually it is referring to degrees of pass/fail, eg an examination; whereas this is about 'readability'. Perhaps a (class) docstring would have helped, but silly-boys (like...) would probably jump to the incorrect assumption anyway... Yes, the US uses scores as a grade, and also school "years" as grades too. A first year student on regular school is said to be in "First Grade". The report score is literally referred to as a "Grade level". I do need more comments, though. If other countries use a different scale for assessing reading level, that could probably be coded for. Okay, sounds like I have some coding improvements to do, thanks! Leam -- Software Engineer (reuel.net/resume) Scribe: The Domici War (domiciwar.net) General Ne'er-do-well (github.com/LeamHall) From mats at wichmann.us Sun May 5 16:10:36 2024 From: mats at wichmann.us (Mats Wichmann) Date: Sun, 5 May 2024 14:10:36 -0600 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> References: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> Message-ID: <7da7a1bd-8878-4204-92a3-ff7be3136f8c@wichmann.us> On 5/4/24 09:15, Leam Hall wrote: > This gets a rough USA grade level report for chapters in a book. There's > a lot of "count()" duplication, can it be done in a cleaner fashion? > Coding style is Python 3.12 and standard library only; no exceptions. I > also run "black -l 79" on everything.?? :) > > Full code is at:? https://github.com/LeamHall/bookbot > ... > ??? def count_sentences(self): > ??????? """Counts the number of sentence ending marks.""" > ??????? self.sentence_count = 0 > ??????? for line in self.lines: > ??????????? self.sentence_count += line.count(".") > ??????????? self.sentence_count += line.count("?") > ??????????? self.sentence_count += line.count("!") So to swing back to the original question which seemed to be more about "I'm counting things over and over...is that bad?" the "hard part" of this work is splitting up the text in a way that identifies the different things you want to tot up, and the easier part is doing the computations once you've done so. Making sense of the English language (or in fact, any human language - in contrast to computer languages which have well-defined parsers) is a long-standing problem that lots of people work on. These days most people choose, for production use, to stand on the shoulders of those who have gone before, rather than rolling their own. Of course, if you're coding your own project as a learning exercise, of course the balance of factors is different. All this to say, someday, as a different exercise, you may want to experiment with using something like NLTK, which is a mature (thousands of commits across over 20 years of development history) toolkit for working with natural language. (https://github.com/nltk/nltk) From phillor9 at gmail.com Tue May 7 00:41:00 2024 From: phillor9 at gmail.com (Phil) Date: Tue, 7 May 2024 14:41:00 +1000 Subject: [Tutor] Centring an image on a label Message-ID: Spurred on by a recent question about displaying images, I have developed an image viewer that works except I haven't been able to centre the image when the image is less than the width or height of the frame. I have label on a frame. The frame is set to 600 x 600. Images that are wider or higher than 600 are scaled to fit. The frame and label are defined as follows: photo_frame = ttk.Frame( ??? root, height=600, width=600, borderwidth=3, relief="sunken") photo_frame.grid(row=0, column=0, padx=10, pady=10, sticky=tk.NSEW) self.photo_label = ttk.Label(photo_frame) self.photo_label.grid(row=0, column=0, padx=0, pady=0)#sticky=tk.NSEW) root.grid_rowconfigure(0, weight=1) root.grid_columnconfigure(0, weight=1) photo_frame.grid_propagate(False) I have a test image that requires a padx of 63 to be centred and I'm trying to do that as follows: self.photo_label.configure(padx=63) Resetting the padding before the image is displayed has no effect and and neither does resetting the padding after the image is displayed. photo = ImageTk.PhotoImage(image) self.photo_label.config(image=photo) self.photo_label.image = (photo) It appears that the way that I'm trying to centre the image is not the correct method. I had a play with the pack manager but that didn't seem to be the answer either. Is it possible the centre the image without calculating the required padding? Any suggestions will be greatly appreciated. -- Regards, Phil From leamhall at gmail.com Tue May 7 07:38:35 2024 From: leamhall at gmail.com (Leam Hall) Date: Tue, 7 May 2024 06:38:35 -0500 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: References: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> <645c086b-9895-4767-d6d2-f535824aa714@gmail.com> Message-ID: <3ec3f123-2979-35cc-d764-a870d6820e60@gmail.com> On 5/4/24 17:42, Alan Gauld via Tutor wrote: > BTW. For a slightly different solution to a very similar problem > check out the grammar checker case study in my tutorial... > It doesn't do the "level report" but it does count the > various grammar parts. It uses regex for the punctuation/splitting etc Hey Alan, I just went looking for this and had forgotten how in depth your tutorial is! Which section is this in? Is http://alan-g.me.uk/l2p2/index.htm the right URL? Thanks! Leam -- DevSecOps Engineer (reuel.net/resume) Scribe: The Domici War (domiciwar.net) General Ne'er-do-well (github.com/LeamHall) From leamhall at gmail.com Tue May 7 07:43:18 2024 From: leamhall at gmail.com (Leam Hall) Date: Tue, 7 May 2024 06:43:18 -0500 Subject: [Tutor] Ideas on making this cleaner? In-Reply-To: <3ec3f123-2979-35cc-d764-a870d6820e60@gmail.com> References: <237e8360-d333-d6ab-8eec-9406984820e9@gmail.com> <645c086b-9895-4767-d6d2-f535824aa714@gmail.com> <3ec3f123-2979-35cc-d764-a870d6820e60@gmail.com> Message-ID: Ah, dug in some more, it's in "A Case Study". On 5/7/24 06:38, Leam Hall wrote: > On 5/4/24 17:42, Alan Gauld via Tutor wrote: > >> BTW. For a slightly different solution to a very similar problem >> check out the grammar checker case study in my tutorial... >> It doesn't do the "level report" but it does count the >> various grammar parts. It uses regex for the punctuation/splitting etc > > Hey Alan, I just went looking for this and had forgotten how in depth your tutorial is! Which section is this in? Is http://alan-g.me.uk/l2p2/index.htm the right URL? > > Thanks! > > Leam > -- DevSecOps Engineer (reuel.net/resume) Scribe: The Domici War (domiciwar.net) General Ne'er-do-well (github.com/LeamHall) From phillor9 at gmail.com Wed May 8 23:23:38 2024 From: phillor9 at gmail.com (Phil) Date: Thu, 9 May 2024 13:23:38 +1000 Subject: [Tutor] tkinter ttk style Message-ID: <6656b06f-f0db-4f82-8cc6-086d9bc4b8f5@gmail.com> I've spent a lot of time reading documentation and tying to adapt examples to set two ttk buttons to different background colours without success. Using the following code both buttons have the same background colour. ??????? self.style = ttk.Style() ??????? self.style.configure("TButton", background="orange") ??????? self.style.configure("TButton.another_button", background="pink") ??????? exit_button = ttk.Button(frame, text="Exit", command=self.destroy) ??????? exit_button.grid(column=3, row=3, padx=5, pady=5) ??????? anothert_button = ttk.Button(frame, etc) I've even resorted to asking my AI friend for help and eventually I got working code that included the following. Although it works almost perfectly (the pink button is smaller than the exit button for some reason) the code seems to be unnecessarily convoluted. ??????? self.style.layout( ??????????? "TButton.another_button", ??????????? [ ??????????????? ( ??????????????????? "Button.focus", ??????????????????? { ??????????????????????? "children": [ ??????????????????????????? ( ??????????????????????????????? "Button.padding", ??????????????????????????????? {"children": [("Button.label", {"sticky": "nswe"})]}, ??????????????????????????? ) ??????????????????????? ] ??????????????????? }, ??????????????? ) ??????????? ], ??????? ) Again, any tips will be greatly appreciated. -- Regards, Phil From phillor9 at gmail.com Wed May 8 23:40:46 2024 From: phillor9 at gmail.com (Phil) Date: Thu, 9 May 2024 13:40:46 +1000 Subject: [Tutor] ttk style question - solved Message-ID: Sorry to have bothered everyone, I just stumbled upon the answer. -- Regards, Phil From mikeatwh at talktalk.net Thu May 9 08:48:31 2024 From: mikeatwh at talktalk.net (Mike) Date: Thu, 9 May 2024 13:48:31 +0100 Subject: [Tutor] Split Drive Setup Message-ID: Hi, I?m using a new 64bit windows11 platform to install python 3.11.3 which I download from the official Python site. I ran the setup programme on my system pc disk C:\ , but changed the location for the bulk of the python files to an external usb HDD to (P:\python\python_3_11_3) , so as not to use a large amount of space on my system drive & have a portable python facility on the USB HDD. The installation completed without any errors and placed an icon on my pc to launch the python command window , which works. I created a one line test script on the pc (print (?hello python !!?) with a ?py? file type label and then double clicked that file but nothing happened .? Can anyone help to solve this problem or indicate other things I need to done, or should have done ? , on my PC or my external HDD MikeH Sent from my iPhone (TalkTalk) From mats at wichmann.us Thu May 9 16:44:24 2024 From: mats at wichmann.us (Mats Wichmann) Date: Thu, 9 May 2024 14:44:24 -0600 Subject: [Tutor] Split Drive Setup In-Reply-To: References: Message-ID: <7eed02ee-a032-4313-84e5-6b1cd33a80a7@wichmann.us> On 5/9/24 06:48, Mike via Tutor wrote: > Hi, > I?m using a new 64bit windows11 platform to install python 3.11.3 which I download from the official Python site. > I ran the setup programme on my system pc disk C:\ , but changed the location for the bulk of the python files to an external usb HDD to (P:\python\python_3_11_3) , so as not to use a large amount of space on my system drive & have a portable python facility on the USB HDD. > The installation completed without any errors and placed an icon on my pc to launch the python command window , which works. > I created a one line test script on the pc (print (?hello python !!?) with a ?py? file type label and then double clicked that file but nothing happened .? > Can anyone help to solve this problem or indicate other things I need to done, or should have done ? , on my PC or my external HDD try running the script from a cmd shell to verify the installation works. the click-on-icon-to-run functionality is a different beast - the Python interpreter is a command-line oriented thing. It's certainly possible to make desktop icons work, maybe with some extra settings, but suggest verifying you have a working installation to begin with. From alan.gauld at yahoo.co.uk Thu May 9 18:44:19 2024 From: alan.gauld at yahoo.co.uk (Alan Gauld) Date: Thu, 9 May 2024 23:44:19 +0100 Subject: [Tutor] Split Drive Setup In-Reply-To: References: Message-ID: On 09/05/2024 13:48, Mike via Tutor wrote: > ...have a portable python facility on the USB HDD. That may not work as you'd hope. I think the install puts some files into system libraries as well as the "install directory". But I may be wrong. I know there are "Python on a USB stick" distributions on the web. > I created a one line test script on the pc > (print (?hello python !!?) with a ?py? file type > label and then double clicked that file but nothing happened .? I suspect the interpreter started, ran your script then stopped and closed before you could see it. Try adding the line input("Hit enter to close...") at the end and see if anything appears. If not start a CMD prompt and run the script manually: C:\WINDOWS> py myscript.py And see if you get any error messages. -- Alan G Author of the Learn to Program web site http://www.alan-g.me.uk/ http://www.amazon.com/author/alan_gauld Follow my photo-blog on Flickr at: http://www.flickr.com/photos/alangauldphotos From mikeatwh at talktalk.net Sat May 11 07:05:37 2024 From: mikeatwh at talktalk.net (MikeH) Date: Sat, 11 May 2024 12:05:37 +0100 Subject: [Tutor] NETGEAR NAS DUO 2110 setup on WIN11platform Message-ID: <6CC72F9F-EE67-4415-A4D5-A8DA60BD4062@talktalk.net> Hi, I have had this NAS for many years it it has worked like a charm on my windowsXP 0S laptop. Recently I purchase a new Asustor laptop running WIn11 and I cannot get it to work on this PC. The device shows as storage device on my home screen and in file explorer with its IP & I can sucesfully ping it. When I try to login with its IP , I receive an error message which says ??.this browser (google) has no support for this interface with an error message code Ox0004002? Searching on the web indicates this may be because a communications protocol TLS 1.0 or 1.1 is no longer supported, or advised , on the windows 10/11 platform which the NAS?s software uses. Has anyone else had this issue and is there a solution or work round. I have done a factory reset on the NAS. Thanks MikeH Sent from my iPad From alan.gauld at yahoo.co.uk Sat May 11 14:36:03 2024 From: alan.gauld at yahoo.co.uk (Alan Gauld) Date: Sat, 11 May 2024 19:36:03 +0100 Subject: [Tutor] NETGEAR NAS DUO 2110 setup on WIN11platform In-Reply-To: <6CC72F9F-EE67-4415-A4D5-A8DA60BD4062@talktalk.net> References: <6CC72F9F-EE67-4415-A4D5-A8DA60BD4062@talktalk.net> Message-ID: On 11/05/2024 12:05, MikeH via Tutor wrote: This really is way off topic for this forum, having nothing at all to do with Python or programming. However... > The device shows as storage device on my home screen and > in file explorer with its IP & I can sucesfully ping it. > When I try to login with its IP , I receive an error > message which says ??.this browser (google) has no support > for this interface with an error message code Ox0004002? So have you tried another browser? Edge or Chrome for example? The admin screen should be using a standard HTML/HTTP interface so you should be able to get in and change the NAS protocol and/or security settings. > I have done a factory reset on the NAS. That should let you use the http address of the admin screen. I hope you know/remember the admin login! -- Alan G Author of the Learn to Program web site http://www.alan-g.me.uk/ http://www.amazon.com/author/alan_gauld Follow my photo-blog on Flickr at: http://www.flickr.com/photos/alangauldphotos