vicary 0.2.16 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e78fbd85fa4dde2e481bbf8b5349ceda35e4ab98fbf9ad0b336761be3f6379d
4
- data.tar.gz: b9ad9cd2ebeaffa8b438c4b051d378edb47cfc86f1117cce68732f6cd3f2cdd3
3
+ metadata.gz: 96894f65cf1e88e86b89131843ae808215e94c6df4ce8e62b47286fd8ba60431
4
+ data.tar.gz: 3054de303e6ba58b370a27d34d96b046ba27878f2bcbe8e40efa0c421c09f1b3
5
5
  SHA512:
6
- metadata.gz: 9ef2a213c75f9cf539620a393b20e7f2521173fb14a2bfbc2d6753654a8a19f19875b143a6b4240cc4ce09b01667cccc4d1da8d2e4c52d774bc9678c52eb13cb
7
- data.tar.gz: 7102716c3687292aff85bc8191e0fbbe65ef457930f9fcce2c5b78e58ed3134a8e69d5b07f728a09438f0aeda84f97753a82419e91478ebd401ba99df5c2dd47
6
+ metadata.gz: 80246adb714cb8a2d32794187fafdd80cc58b573bf633cf5f6c89528f89cc71624e1bba013eea26f6b0c7fdf238f8d7e033b25391b60fd7338c7439ad8e823fd
7
+ data.tar.gz: 9c1b2a572734f144b49cc770be0d731f4b3559d88d020b9a6be77d78c9dd24faa1a25219a5a1274e46c1e3697c87c569e36d7b9228acd84769e24fcb1faed1ce
data/assets/MANIFEST.json CHANGED
@@ -22,11 +22,11 @@
22
22
  }
23
23
  },
24
24
  "stop_words_never_capitalised.txt": {
25
- "bytes": 8017,
26
- "entries": 499,
25
+ "bytes": 8108,
26
+ "entries": 511,
27
27
  "format": 1,
28
28
  "min_package_version": "0.2.10",
29
- "sha256": "23798cb88672c126d71a9d0988b9adf0aa044bdc291847012d76deae71fe1d33"
29
+ "sha256": "809e92aa74f001533be8bd33bfe119f5edab4bef2fbde687140f16c998c1039f"
30
30
  },
31
31
  "stop_words_sometimes_capitalised.txt": {
32
32
  "bytes": 6351,
@@ -37,5 +37,5 @@
37
37
  }
38
38
  },
39
39
  "manifest_version": 1,
40
- "written_by": "vicary 0.2.13"
40
+ "written_by": "vicary 0.2.17"
41
41
  }
@@ -1,5 +1,5 @@
1
1
  #!lexicon 1
2
- #!list stop_words_never_capitalised 499
2
+ #!list stop_words_never_capitalised 511
3
3
  # Ordinary words that must never become name candidates, and that English never
4
4
  # capitalises in the middle of a sentence.
5
5
  #
@@ -58,6 +58,7 @@
58
58
  a an the this that these those there here it its it's
59
59
  i me my mine myself we us our ours ourselves you your yours
60
60
  he him his she her hers they them their theirs who whom whose which what
61
+ how why where wherever whatever whoever
61
62
  and or but so because although though however therefore thus hence yet
62
63
  if then else when while until since before after during once whenever
63
64
  for from to into onto out off over under above below between among across
@@ -109,18 +110,19 @@
109
110
  cants coulds didnts dids doeses doings donts durings eithers elses etcs
110
111
  everies everybodies everyones everythings everywheres feels fews finallies
111
112
  finds froms gaves gets gives gos gots growings hads hases havings helpeds
112
- hences hims hises howevers ids ifs ills ins insteads intos ises isnts justs
113
- keeps kepts knews knows learneds learnings learns leaves lefts lets lookeds
114
- lookings looks makes maybes mights mosts movings muches musts myselfs
115
- neededs neithers nobodies nos nothings nots nowheres nows offs oftens okays
116
- onces onlies ons ontos ourselveses outs perhapses playings pluses puts
117
- quites rarelies rathers reads reallies remembereds remembers runnings saids
118
- saws says sets severals shalls shes shoulds sinces sittings somebodies
119
- someones somethings sometimeses somewheres standings starteds stoppeds
120
- suches talkings taughts teaches tells thans thems thens therefores thes
121
- theses thinkings thinks thises thoses thoughs thoughts throughs thuses
113
+ hences hims hises howevers hows ids ifs ills ins insteads intos ises isnts
114
+ justs keeps kepts knews knows learneds learnings learns leaves lefts lets
115
+ lookeds lookings looks makes maybes mights mosts movings muches musts
116
+ myselfs neededs neithers nobodies nos nothings nots nowheres nows offs
117
+ oftens okays onces onlies ons ontos ourselveses outs perhapses playings
118
+ pluses puts quites rarelies rathers reads reallies remembereds remembers
119
+ runnings saids saws says sets severals shalls shes shoulds sinces sittings
120
+ somebodies someones somethings sometimeses somewheres standings starteds
121
+ stoppeds suches talkings taughts teaches tells thans thems thens therefores
122
+ thes theses thinkings thinks thises thoses thoughs thoughts throughs thuses
122
123
  todays togethers tolds tomorrows toos trieds tries tryings unders unlesses
123
124
  untils ups uses usings usuallies veries versuses walkings wanteds wants
124
- wases wents weres whenevers whens whethers whiches whoms whos whoses withins
125
- withouts wonts workeds woulds writes wrotes yeahs yeses yesterdays yets yous
125
+ wases wents weres whatevers whenevers whens wheres wherevers whethers
126
+ whiches whies whoevers whoms whos whoses withins withouts wonts workeds
127
+ woulds writes wrotes yeahs yeses yesterdays yets yous
126
128
  # <<< end generated inflections
@@ -257,6 +257,16 @@ module Vicary
257
257
  # Matches the abbreviation a break candidate sits directly behind.
258
258
  TRAILING_WORD = /([A-Za-z]+)\.\s*\z/
259
259
 
260
+ # Words that name the letter after them as a letter: "a capital C", "a big M".
261
+ # Feedback on spelling and capitals says this all the time, and the letter
262
+ # then stood alone mid-sentence with nothing to tell it from an initial.
263
+ # Measured on 696 generated replies: 4 of the 39 placeholders a host's guard
264
+ # had to remove.
265
+ LETTER_NAMING_WORDS = Set.new(%w[capital big small lowercase uppercase letter]).freeze
266
+
267
+ # The word directly before a span, read back over whitespace and an opening quote.
268
+ PRECEDING_WORD = /([A-Za-z]+)[\s'"‘“]*\z/
269
+
260
270
  # {SENTENCE_BREAK} with the `\A` arm removed, so it cannot match empty.
261
271
  #
262
272
  # A dialect difference, and it moved a number. `SENTENCE_BREAK` matches the
@@ -300,6 +310,39 @@ module Vicary
300
310
  most much many few several enough
301
311
  ]).freeze
302
312
 
313
+ # Phonetic spellings of English stop words, each with the stop word it
314
+ # spells. Young writers, and young Spanish-L1 writers most of all,
315
+ # spell the closed-class words by ear — "dey sai ai smel", "so ai dont get
316
+ # mai clos dirtier" — and several of those spellings are given names the
317
+ # tier knows, so the lowercase route masked "sai ai" and "mai clos" and the
318
+ # sentence-initial rule let the tier vouch for "Ai", "Mi" and "De". A
319
+ # spelling of a stop word is the same word, so in a document that writes
320
+ # this way it is a stop word too. Only the spellings are read; the pairing
321
+ # is documentation. See `phonetic_function_words` for when a document
322
+ # qualifies.
323
+ PHONETIC_FUNCTION_WORDS = {
324
+ "ai" => "i", "becuz" => "because", "bicuz" => "because", "bot" => "but",
325
+ "cud" => "could", "da" => "the", "dan" => "than", "dat" => "that",
326
+ "de" => "the", "deir" => "their", "dem" => "them", "den" => "then",
327
+ "dere" => "there", "dese" => "these", "dey" => "they", "dis" => "this",
328
+ "dose" => "those", "frum" => "from", "hiz" => "his", "iz" => "is",
329
+ "mai" => "my", "mi" => "me", "sai" => "say", "sed" => "said",
330
+ "shud" => "should", "sum" => "some", "tu" => "to", "wat" => "what",
331
+ "wen" => "when", "wud" => "would", "wuz" => "was", "yu" => "you",
332
+ "yur" => "your",
333
+ }.freeze
334
+
335
+ # Distinct phonetic spellings a document must write in lower case before
336
+ # any of them is read as a word rather than a name. One is a coincidence —
337
+ # a friend called Mai — two different ones is a writer spelling by ear.
338
+ PHONETIC_REGISTER_MIN = 2
339
+
340
+ PHONETIC_TOKEN = Regexp.new(
341
+ "(?<![A-Za-z'’-])(?:" +
342
+ PHONETIC_FUNCTION_WORDS.keys.sort_by { |w| [-w.length, w] }.join("|") +
343
+ ")(?![A-Za-z'’-])"
344
+ )
345
+
303
346
  HONORIFIC_SET = Set.new(HONORIFICS.map(&:downcase)).freeze
304
347
 
305
348
  # One capitalised word, hyphens and apostrophes included so
@@ -742,6 +785,20 @@ module Vicary
742
785
  strip(without_clitic(strip(token.downcase, ".,")), "'’")
743
786
  end
744
787
 
788
+ # The phonetic spellings this document uses as words, to be read as
789
+ # stops. Empty unless the document writes at least PHONETIC_REGISTER_MIN
790
+ # distinct spellings in lower case. When it does, the spellings it writes
791
+ # in lower case — the writer's own testimony that the token is a word to
792
+ # them — less any it also capitalises mid-sentence, which is the opposite
793
+ # testimony and wins.
794
+ def phonetic_function_words(text, written_as_a_capital = Set.new)
795
+ seen = Set.new
796
+ each_match(text, PHONETIC_TOKEN) { |m| seen << m[0] }
797
+ return Set.new if seen.size < PHONETIC_REGISTER_MIN
798
+
799
+ Set.new(seen.reject { |w| written_as_a_capital.include?(w) })
800
+ end
801
+
745
802
  # Whether a capital on `token` mid-sentence is a mistake rather than a
746
803
  # construction — the narrow half of the stoplist.
747
804
  #
@@ -846,7 +903,11 @@ module Vicary
846
903
  # the stoplist so that a bare "Mrs." cannot become a candidate on its own,
847
904
  # but "Mrs. Okonkwo" has to stay whole: masking only the surname leaves the
848
905
  # relationship and the surname's position in the text.
849
- def trim(tokens)
906
+ #
907
+ # `phonetic` is `phonetic_function_words` for the document: spellings read
908
+ # as stop words here. Except a particle *inside* a run — "Maria de Cruz"
909
+ # keeps its `de` however the rest of the document spells "the".
910
+ def trim(tokens, phonetic = Set.new)
850
911
  runs = []
851
912
  current = []
852
913
  tokens.each_with_index do |token, index|
@@ -854,7 +915,10 @@ module Vicary
854
915
  HONORIFIC_SET.include?(strip(token.downcase, ".,")) &&
855
916
  index + 1 < tokens.length &&
856
917
  !stop?(tokens[index + 1])
857
- if stop?(token) && !introduces_a_name
918
+ spelled_stop = !phonetic.empty? && phonetic.include?(fold(token)) &&
919
+ !(index.positive? && index < tokens.length - 1 &&
920
+ PARTICLES.include?(token))
921
+ if (stop?(token) || spelled_stop) && !introduces_a_name
858
922
  unless current.empty?
859
923
  runs << current
860
924
  current = []
@@ -871,6 +935,18 @@ module Vicary
871
935
  # Reading the document's own orthography
872
936
  # ---------------------------------------------------------------------
873
937
 
938
+ # A lone capital that is a label or a letter, never an initial: glued to a
939
+ # digit (`E9`, a quote label the word pattern used to cut at the digit) or
940
+ # named as a letter by the word before it ({LETTER_NAMING_WORDS}). Only a
941
+ # single character is ever released; `Jake2009` stays a candidate.
942
+ def letter_not_a_name?(text, start, finish)
943
+ return false unless finish - start == 1
944
+ return true if finish < text.length && text[finish].match?(/[0-9]/)
945
+
946
+ preceding = PRECEDING_WORD.match(text[0...start])
947
+ !preceding.nil? && LETTER_NAMING_WORDS.include?(preceding[1].downcase)
948
+ end
949
+
874
950
  # Offsets at which a sentence begins.
875
951
  #
876
952
  # A break directly behind a title abbreviation is not one — see
@@ -1849,7 +1925,7 @@ module Vicary
1849
1925
  # capitalisation signal, and the seed stands on the given-name tier
1850
1926
  # alone.
1851
1927
  def find_lowercase_candidates(text, is_given, protected_span, corroborate = nil,
1852
- settlement = nil)
1928
+ settlement = nil, phonetic = Set.new)
1853
1929
  tokens = each_match(text, LOWER_TOKEN).map do |m|
1854
1930
  [m[0], m.begin(0), m.begin(0) + m[0].length]
1855
1931
  end
@@ -1857,7 +1933,10 @@ module Vicary
1857
1933
  index = 0
1858
1934
  while index < tokens.length
1859
1935
  word, start, = tokens[index]
1860
- if stop?(word) || !is_given.call(word)
1936
+ # A phonetic spelling may not SEED a span ("sai ai smel"), but may
1937
+ # still be the token a real seed reaches ("jeni sai") — see the
1938
+ # reference.
1939
+ if stop?(word) || phonetic.include?(word) || !is_given.call(word)
1861
1940
  index += 1
1862
1941
  next
1863
1942
  end
@@ -1996,6 +2075,10 @@ module Vicary
1996
2075
  )
1997
2076
  written_as_a_capital = casing.written_as_a_capital
1998
2077
  lower_cased = casing.written_in_lower_case
2078
+ # Read off the same casing pass, so a spelling the writer capitalises
2079
+ # mid-sentence is never one of them; and only where a given-name oracle
2080
+ # exists, because it only withdraws what the tier proposed or vouched for.
2081
+ phonetic = given_name.nil? ? Set.new : phonetic_function_words(text, written_as_a_capital)
1999
2082
  stray_capitals = mid_sentence_corroboration && !given_name.nil? &&
2000
2083
  capitalises_ordinary_words?(text, headings)
2001
2084
 
@@ -2007,7 +2090,7 @@ module Vicary
2007
2090
  tokens = words(span)
2008
2091
  # A long all-caps run means the capitalisation told us nothing, so the
2009
2092
  # stoplist is carrying the whole decision.
2010
- trim(tokens).each do |run|
2093
+ trim(tokens, phonetic).each do |run|
2011
2094
  next if run.empty?
2012
2095
 
2013
2096
  joined = run.join(" ")
@@ -2017,6 +2100,7 @@ module Vicary
2017
2100
 
2018
2101
  start = m.begin(0) + offset
2019
2102
  next if is_protected.call(start, start + joined.length)
2103
+ next if letter_not_a_name?(text, start, start + joined.length)
2020
2104
 
2021
2105
  # Cheapest of the three capital-discounting rules and the only one
2022
2106
  # that reads neither position nor list, so it goes first.
@@ -2078,7 +2162,7 @@ module Vicary
2078
2162
  # evidence to offer and is better served by it.
2079
2163
  find_lowercase_candidates(
2080
2164
  text, given_name, is_protected,
2081
- habit == LOWERCASE ? nil : written_as_a_capital, settlement
2165
+ habit == LOWERCASE ? nil : written_as_a_capital, settlement, phonetic
2082
2166
  ).each do |candidate|
2083
2167
  next if claimed.any? { |s, e| candidate.start < e && candidate.end > s }
2084
2168
 
@@ -6,5 +6,5 @@ module Vicary
6
6
  # Shared across all three front doors on purpose: one detector, one number. A
7
7
  # gem 0.3.0 that corresponds to nothing on PyPI cannot be reasoned about, and
8
8
  # the parity claim is between *versions*, not between package names.
9
- VERSION = "0.2.16"
9
+ VERSION = "0.2.18"
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vicary
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.16
4
+ version: 0.2.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blake Thomas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-19 00:00:00.000000000 Z
11
+ date: 2026-10-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Finds the names a student writes about — classmates, teachers, relatives — and