packages feed

htsn-import 0.2.1 → 0.2.2

raw patch · 21 files changed

+165/−72 lines, 21 files

Files

doc/TODO view
@@ -1,15 +1,9 @@-1. Re-enable DTD validation once-   https://github.com/UweSchmidt/hxt/issues/15 is fixed.--2. Write a test for test/xml/Odds_XML-long-import.xml once it no-   longer takes 10 minutes to import (Postgres only?).--3. We have DTDs but no sample XML for the following SportInfo types,+1. We have DTDs but no sample XML for the following SportInfo types,    which have therefore been left unimplmented for now:     * CBASK_StealsXML.dtd -4. The following DTD types were handled (in some form) by the old+2. The following DTD types were handled (in some form) by the old    FeedGrabber. They are not yet handled by htsn-import (some may not    be valid): @@ -45,10 +39,13 @@    * WNBA_Individual_Stats_XML    * WNBATeamScheduleXML -5. Consolidate all of the make_game_time functions which take a+3. Consolidate all of the make_game_time functions which take a    date/time and produce a combined time. -6. Factor out test code where possible; a lot of them differ only in+4. Factor out test code where possible; a lot of them differ only in    the filename. -7. Combine test XML files where possible.+5. Combine test XML files where possible. We don't need to e.g. import+   two copies of newsxml just because one has an empty attribute that+   we want to test: we could just delete an attribute from the first+   file.
htsn-import.cabal view
@@ -1,5 +1,5 @@ name:           htsn-import-version:        0.2.1+version:        0.2.2 cabal-version:  >= 1.8 author:         Michael Orlitzky maintainer:	Michael Orlitzky <michael@orlitzky.com>@@ -283,6 +283,7 @@     Configuration     ConnectionString     ExitCodes+    Misc     OptionalConfiguration     TSN.Codegen     TSN.Database
schema/earlylineXML.dtd view
@@ -9,7 +9,7 @@ <!ELEMENT teamH (#PCDATA)> <!ELEMENT over_under (#PCDATA)> <!ELEMENT game ( ( time, teamA, teamH, over_under ) )>-<!ELEMENT date ( ( note, game ) )>+<!ELEMENT date ( ( note?, game )* )> <!ELEMENT time_stamp (#PCDATA)> <!ELEMENT message ( ( XML_File_ID, heading, category, sport, title, date*, time_stamp ) )> 
schema/nflstandxml.dtd view
@@ -6,7 +6,7 @@ <!ELEMENT team (#PCDATA)> <!ELEMENT won (#PCDATA)> <!ELEMENT lost (#PCDATA)>-<!ELEMENT tied EMPTY>+<!ELEMENT tied (#PCDATA)> <!ELEMENT percentage (#PCDATA)> <!ELEMENT PF (#PCDATA)> <!ELEMENT PA (#PCDATA)>
+ schemagen/earlylineXML/22064237.xml view
@@ -0,0 +1,1 @@+<?xml version="1.0" standalone="no" ?>
<!DOCTYPE message PUBLIC "-//TSN//DTD Odds 1.0/EN" "earlylineXML.dtd">
<message>
<XML_File_ID>22064237</XML_File_ID>
<heading>ADO;NHL-EARLY-LINE</heading>
<category>Odds</category>
<sport>NHL</sport>
<title>National Hockey League Overnight Line</title>
<date value="TUESDAY, OCTOBER 28TH (10/28/2014)">
<game>
<time></time>
<teamA rotation="" line=""></teamA>
<teamH rotation="001" line="">Ottawa</teamH>
<over_under></over_under>
</game>
<game>
<time></time>
<teamA rotation="" line="5.5u">Over/Under:</teamA>
<teamH rotation="" line=""></teamH>
<over_under></over_under>
</game>
<game>
<time></time>
<teamA rotation="004" line="-170">Pittsburgh</teamA>
<teamH rotation="" line="5.5u">Over/Under:</teamH>
<over_under></over_under>
</game>
<game>
<time>7:00</time>
<teamA rotation="005" line="">Los Angeles</teamA>
<teamH rotation="006" line="off">Philadelphia</teamH>
<over_under></over_under>
</game>
<game>
<time>7:00</time>
<teamA rotation="007" line="">Winnipeg</teamA>
<teamH rotation="008" line="-160">NY Islanders</teamH>
<over_under>5.5o</over_under>
</game>
<game>
<time>7:00</time>
<teamA rotation="009" line="">Minnesota</teamA>
<teamH rotation="010" line="-160">Boston</teamH>
<over_under>5u</over_under>
</game>
<game>
<time>7:30</time>
<teamA rotation="011" line="">Buffalo</teamA>
<teamH rotation="012" line="-220">Toronto</teamH>
<over_under>5.5u</over_under>
</game>
<game>
<time>7:30</time>
<teamA rotation="013" line="">Arizona</teamA>
<teamH rotation="014" line="-170">Tampa Bay</teamH>
<over_under>5.5p</over_under>
</game>
<game>
<time>8:30</time>
<teamA rotation="015" line="">St. Louis</teamA>
<teamH rotation="016" line="-120">Dallas</teamH>
<over_under>5o</over_under>
</game>
<game>
<time>8:30</time>
<teamA rotation="017" line="">Anaheim</teamA>
<teamH rotation="018" line="-150">Chicago</teamH>
<over_under>5.5u</over_under>
</game>
<game>
<time>9:00</time>
<teamA rotation="019" line="-120">San Jose</teamA>
<teamH rotation="020" line="">Colorado</teamH>
<over_under>5.5p</over_under>
</game>
<game>
<time>9:00</time>
<teamA rotation="021" line="">Montreal</teamA>
<teamH rotation="022" line="off">Calgary</teamH>
<over_under></over_under>
</game>
<game>
<time>10:00</time>
<teamA rotation="023" line="">Carolina</teamA>
<teamH rotation="024" line="-220">Vancouver</teamH>
<over_under>5.5u</over_under>
</game>
</date>
<time_stamp> October 27, 2014, at 02:44 PM ET </time_stamp>
</message>
src/Main.hs view
@@ -22,6 +22,7 @@ import Text.XML.HXT.Core (   ArrowXml,   IOStateArrow,+  SysConfigList,   XmlTree,   (>>>),   (/>),@@ -83,7 +84,7 @@   is_type1,   pickle_message,   teams_are_normal )-import Xml ( DtdName(..), parse_opts )+import Xml ( DtdName(..), parse_opts, parse_opts_novalidate )   -- | This is where most of the work happens. This function is called@@ -140,9 +141,11 @@       -- we couldn't parse the DTD.       return [ImportFailed errdesc] -    -- | An arrow that reads a document into an 'XmlTree'.-    readA :: IOStateArrow s a XmlTree-    readA = readDocument parse_opts path+    -- | An arrow that reads a document into an 'XmlTree'.  We take a+    --   SysConfigList so our caller can decide whether or not to+    --   e.g. validate the document against its DTD.+    readA :: SysConfigList -> IOStateArrow s a XmlTree+    readA scl = readDocument scl path      -- | An arrow which parses the doctype "SYSTEM" of an 'XmlTree'.     --   We use these to determine the parser to use.@@ -156,11 +159,27 @@     --   The result of runX has type IO [IO ImportResult]. We thus use     --   bind (>>=) and sequence to combine all of the IOs into one     --   big one outside of the list.+    --+    --   Before we actually run the import, we check it against a list+    --   of problem DTDs. These can produce weird errors, and we have+    --   checks for them. But with DTD validation enabled, we can't+    --   even look inside the document to see what's wrong -- parsing+    --   will fail! So for those special document types, we proceed+    --   using 'parse_opts_novalidate' instead of the default+    --   'parse_opts'.+    --     parse_and_import :: IO [ImportResult]-    parse_and_import =-      runX (readA >>> (dtdnameA &&& returnA) >>> (arr import_with_dtd))-      >>=-      sequence+    parse_and_import = do+      -- Get the DTD name without validating against it.+      ((DtdName dtd) : _) <- runX $ (readA parse_opts_novalidate) >>> dtdnameA++      let problem_dtds = [ News.dtd, Weather.dtd ]+      let opts = if dtd `elem` problem_dtds+                 then parse_opts_novalidate+                 else parse_opts++      runX ((readA opts) >>> (dtdnameA &&& returnA) >>> (arr import_with_dtd))+        >>= sequence      -- | Takes a ('DtdName', 'XmlTree') pair and uses the 'DtdName'     --   to determine which function to call on the 'XmlTree'.
+ src/Misc.hs view
@@ -0,0 +1,24 @@+-- | Miscellaneous utility functions+module Misc (+  double_just )+where++-- | If given 'Nothing', return 'Nothing'. Otherwise wrap our argument+--   in another 'Just'. This is used when handling optional XML+--   elements that are optionally empty. If the element is missing, we+--   want 'Nothing'. And if the contents are missing, we want+--   'Nothing' then too. But if something is present, we need @Just+--   (Just foo)@ for the types to match up.+--+--   ==== __Examples__:+--+--   >>> double_just Nothing+--   Nothing+--   >>> double_just (Just 2)+--   Just (Just 2)+--+double_just :: (Maybe a) -> Maybe (Maybe a)+double_just val =+  case val of+    Nothing -> Nothing+    just_something -> Just just_something
src/TSN/DbImport.hs view
@@ -27,12 +27,11 @@ import Control.Monad.IO.Class ( MonadIO( liftIO ) ) import qualified Data.Map as Map ( elems ) import Database.Groundhog ( executeRaw )-import Database.Groundhog.Core ( NamedMigrations ) import Database.Groundhog.Generic (   createMigration,   getQueries,   mergeMigrations )-import Database.Groundhog.Core ( Migration, PersistBackend )+import Database.Groundhog.Core ( Migration, NamedMigrations, PersistBackend ) import Network.Services.TSN.Report ( report_info )  
src/TSN/Picklers.hs view
@@ -1,9 +1,12 @@+{-# LANGUAGE ScopedTypeVariables #-}+ -- | (Un)picklers for data types present in The Sports Network XML --   feed. -- module TSN.Picklers (   pickler_tests,   xp_ambiguous_time,+  xp_attr_option,   xp_date,   xp_date_padded,   xp_datetime,@@ -568,6 +571,23 @@         upper_suffix = map toUpper (date_suffix t)         fmt = "%A, %B %-d" ++ upper_suffix ++ " (" ++ date_format_padded ++ ")" ++-- | This is a replacement for @xpOption xpFoo@ within an 'xpAttr'.+--   There's a bug <https://github.com/UweSchmidt/hxt/issues/39> in+--   newer versions of HXT that prevents us from using the usual+--   'xpOption' solution, so this is our stopgap. It should work on+--   any type that can be unpickled with a plain read/show.+--+xp_attr_option ::  forall a. (Read a, Show a) => PU (Maybe a)+xp_attr_option =+  (to_a, from_a) `xpWrap` xpText+  where+    to_a :: String -> Maybe a+    to_a = readMaybe++    from_a :: Maybe a -> String+    from_a Nothing  = ""+    from_a (Just x) = show x   -- | Create an 'XmlTree' containing only the given text. This is
src/TSN/XML/EarlyLine.hs view
@@ -67,10 +67,12 @@   xpWrap )  -- Local imports.+import Misc ( double_just ) import TSN.Codegen ( tsn_codegen_config ) import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate ) import TSN.Picklers (   xp_ambiguous_time,+  xp_attr_option,   xp_early_line_date,   xp_time_stamp ) import TSN.XmlImport ( XmlImport(..) )@@ -559,7 +561,7 @@ pickle_team :: PU EarlyLineGameTeamXml pickle_team =   xpWrap (from_tuple, to_tuple') $-  xp6Tuple (xpAttr "rotation" (xpOption xpInt))+  xp6Tuple (xpAttr "rotation" xp_attr_option)            (xpOption $ xpAttr "line" (xpOption xpText))            (xpOption $ xpAttr "name" xpText)            (xpOption xpText)@@ -571,10 +573,6 @@      to_tuple' (EarlyLineGameTeamXml u v w x y z) =       (u, double_just v, w, x, double_just y, double_just z)-      where-        double_just val = case val of-               Nothing -> Nothing-               just_something -> Just just_something   
src/TSN/XML/JFile.hs view
@@ -23,7 +23,7 @@ where  -- System imports-import Control.Monad ( forM_ )+import Control.Monad ( forM_, join ) import Data.List ( intercalate ) import Data.String.Utils ( split ) import Data.Time ( UTCTime(..) )@@ -61,6 +61,7 @@   -- Local imports+import Misc ( double_just ) import TSN.Codegen ( tsn_codegen_config ) import TSN.Database ( insert_or_select ) import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate )@@ -458,7 +459,7 @@ pickle_game :: PU JFileGameXml pickle_game =   xpElem "game" $-    xpWrap (from_tuple, H.convert) $+    xpWrap (from_tuple, to_tuple') $     xp14Tuple (xpElem "game_id" xpInt)               (xpElem "schedule_id" xpInt)               pickle_odds_info@@ -466,7 +467,7 @@               (xpElem "Game_Date" xp_date_padded)               (xpElem "Game_Time" xp_tba_time)               pickle_away_team-              (xpOption $ xpElem "vleague" xpText)+              (xpOption $ xpElem "vleague" (xpOption xpText))               pickle_home_team               (xpOption $ xpElem "hleague" xpText)               (xpElem "vscore" xpInt)@@ -474,7 +475,11 @@               (xpOption $ xpElem "time_r" xpText)               pickle_status   where-    from_tuple = uncurryN JFileGameXml+    from_tuple (a,b,c,d,e,f,g,h,i,j,k,l,m,n) =+      JFileGameXml a b c d e f g (join h) i j k l m n++    to_tuple' (JFileGameXml a b c d e f g h i j k l m n) =+      (a, b, c, d, e, f, g, double_just h, i, j, k, l, m, n)   pickle_odds_info :: PU JFileGameOddsInfo
src/TSN/XML/News.hs view
@@ -72,7 +72,7 @@ import TSN.Database ( insert_or_select ) import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate ) import TSN.Location ( Location(..), pickle_location )-import TSN.Picklers ( xp_time_stamp )+import TSN.Picklers ( xp_attr_option, xp_time_stamp ) import TSN.Team ( Team(..) ) import TSN.XmlImport ( XmlImport(..) ) import Xml (@@ -80,7 +80,7 @@   ToDb(..),   pickle_unpickle,   unpickleable,-  unsafe_read_document,+  unsafe_read_invalid_document,   unsafe_unpickle )  @@ -389,7 +389,7 @@ pickle_msg_id =   xpElem "msg_id" $     xpWrap (from_tuple, H.convert) $-    xpPair xpInt (xpAttr "EventId" (xpOption xpInt))+    xpPair xpInt (xpAttr "EventId" xp_attr_option)   where     from_tuple = uncurryN MsgId @@ -574,6 +574,6 @@             False ]   where     check path desc expected = testCase desc $ do-      xmltree <- unsafe_read_document path+      xmltree <- unsafe_read_invalid_document path       let actual = has_only_single_sms xmltree       actual @?= expected
src/TSN/XML/Odds.hs view
@@ -66,7 +66,11 @@ import TSN.Codegen ( tsn_codegen_config ) import TSN.Database ( insert_or_select ) import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate )-import TSN.Picklers ( xp_date_padded, xp_tba_time, xp_time_stamp )+import TSN.Picklers (+  xp_attr_option,+  xp_date_padded,+  xp_tba_time,+  xp_time_stamp ) import TSN.Team ( FromXmlFkTeams(..), Team(..) ) import TSN.XmlImport ( XmlImport(..), XmlImportFkTeams(..) ) import Xml (@@ -648,7 +652,7 @@   xpElem "Casino" $   xpWrap (from_tuple, H.convert) $   xpTriple-    (xpAttr "ClientID" $ xpOption xpInt)+    (xpAttr "ClientID" xp_attr_option)     (xpAttr "Name" $ xpOption xpText)     (xpOption xpText)   where@@ -805,7 +809,10 @@           "test/xml/Odds_XML-tba-game-time.xml",      check "pickle composed with unpickle is the identity (empty casino)"-          "test/xml/Odds_XML-empty-casino.xml" ]+          "test/xml/Odds_XML-empty-casino.xml",++    check "pickle composed with unpickle is the identity (long import)"+          "test/xml/Odds_XML-long-import.xml" ]   where     check desc path = testCase desc $ do       (expected, actual) <- pickle_unpickle pickle_message path@@ -838,7 +845,10 @@           "test/xml/Odds_XML-tba-game-time.xml",      check "unpickling succeeds (empty casino)"-          "test/xml/Odds_XML-empty-casino.xml" ]+          "test/xml/Odds_XML-empty-casino.xml",++    check "unpickling succeeds (long-import)"+          "test/xml/Odds_XML-long-import.xml" ]   where     check desc path = testCase desc $ do       actual <- unpickleable path pickle_message@@ -885,6 +895,10 @@     check "deleting odds deleted its children (empty casino)"           "test/xml/Odds_XML-empty-casino.xml"           11 -- 5 casinos, 6 teams+    ,+    check "deleting odds deleted its children (long import)"+          "test/xml/Odds_XML-long-import.xml"+          219 -- 5 casinos, 214 teams     ]   where     check desc path expected = testCase desc $ do
src/TSN/XML/Scores.hs view
@@ -57,11 +57,12 @@   xpWrap )  -- Local imports.+import Misc ( double_just ) import TSN.Codegen ( tsn_codegen_config ) import TSN.Database ( insert_or_select ) import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate ) import TSN.Location ( Location(..), pickle_location )-import TSN.Picklers ( xp_time_stamp )+import TSN.Picklers ( xp_attr_option, xp_time_stamp ) import TSN.Team (   FromXmlFkTeams(..),   HTeam(..),@@ -444,17 +445,13 @@ pickle_status =   xpElem "status" $     xpWrap (from_tuple, to_tuple') $-      xpTriple (xpAttr "numeral" $ xpOption xpInt)+      xpTriple (xpAttr "numeral" xp_attr_option)                (xpOption $ xpAttr "type" $ xpOption xpText)                xpText   where     from_tuple (x,y,z) = ScoreGameStatus x (join y) z     to_tuple' ScoreGameStatus{..} =-      (db_status_numeral, s, db_status_text)-      where-        s = case db_status_type of-              Nothing -> Nothing-              Just _  -> Just db_status_type+      (db_status_numeral, double_just db_status_type, db_status_text)   -- | Convert a 'ScoreGameXml' to/from \<game\>.@@ -522,8 +519,7 @@                (xpOption xpText) -- Team name   where     from_tuple (x,y,z)= HTeamXml (HTeam (Team x Nothing z)) (join y)-    to_tuple' (HTeamXml (HTeam t) Nothing) = (team_id t, Nothing, name t)-    to_tuple' (HTeamXml (HTeam t) jhp) = (team_id t, Just jhp, name t)+    to_tuple' (HTeamXml (HTeam t) jhp) = (team_id t, double_just jhp, name t)   
src/TSN/XML/Weather.hs view
@@ -80,7 +80,7 @@   ToDb(..),   pickle_unpickle,   unpickleable,-  unsafe_read_document,+  unsafe_read_invalid_document,   unsafe_unpickle )  @@ -743,7 +743,7 @@             False ]   where     check path desc expected = testCase desc $ do-      xmltree <- unsafe_read_document path+      xmltree <- unsafe_read_invalid_document path       let actual = is_type1 xmltree       actual @?= expected @@ -764,6 +764,6 @@           False ]   where     check desc path expected = testCase desc $ do-      xmltree <- unsafe_read_document path+      xmltree <- unsafe_read_invalid_document path       let actual = teams_are_normal xmltree       actual @?= expected
src/Xml.hs view
@@ -10,9 +10,11 @@   FromXmlFk(..),   ToDb(..),   parse_opts,+  parse_opts_novalidate,   pickle_unpickle,   unpickleable,   unsafe_read_document,+  unsafe_read_invalid_document,   unsafe_unpickle ) where @@ -97,10 +99,16 @@ -- parse_opts :: SysConfigList parse_opts = [ withRemoveWS yes,-               withSubstDTDEntities no,-               withValidate no ]+               withSubstDTDEntities no ] +-- | Like 'parse_opts' except we don't validate the document against+--   its DTD. This is useful when we need to parse a document that we+--   /know/ is invalid so that we can deliver a better error message.+--+parse_opts_novalidate :: SysConfigList+parse_opts_novalidate = (withValidate no) : parse_opts + -- | Given an @unpickler@ and a @filepath@, attempt to unpickle the --   root element of @filepath@ using @unpickler@ and return both the --   original unpickled object and one constructed by pickling and@@ -174,3 +182,10 @@ unsafe_read_document :: FilePath -> IO XmlTree unsafe_read_document filepath =   fmap head $ runX $ readDocument parse_opts filepath++-- | The same as 'unsafe_read_document', except it allows you to read+--   documents which don't validate against their DTDs.+--+unsafe_read_invalid_document :: FilePath -> IO XmlTree+unsafe_read_invalid_document filepath =+  fmap head $ runX $ readDocument parse_opts_novalidate filepath
test/Doctests.hs view
@@ -9,5 +9,6 @@ main :: IO () main = doctest [ "-isrc",                  "-idist/build/autogen",+                 "src/Misc.hs",                  "src/TSN/Codegen.hs",                  "src/TSN/Picklers.hs"]
test/xml/Odds_XML.dtd view
@@ -13,12 +13,12 @@ <!ELEMENT AwayAbbr (#PCDATA)> <!ELEMENT AwayTeamName (#PCDATA)> <!ELEMENT Casino (#PCDATA)>-<!ELEMENT AwayTeam ( ( AwayTeamID, AwayRotationNumber, AwayAbbr, AwayTeamName, Casino* ) )>+<!ELEMENT AwayTeam ( ( AwayTeamID, AwayRotationNumber, AwayAbbr, AwayTeamName, AStarter?, Casino* ) )> <!ELEMENT HomeTeamID (#PCDATA)> <!ELEMENT HomeRotationNumber (#PCDATA)> <!ELEMENT HomeAbbr (#PCDATA)> <!ELEMENT HomeTeamName (#PCDATA)>-<!ELEMENT HomeTeam ( ( HomeTeamID, HomeRotationNumber, HomeAbbr, HomeTeamName, Casino* ) )>+<!ELEMENT HomeTeam ( ( HomeTeamID, HomeRotationNumber, HomeAbbr, HomeTeamName, HStarter?, Casino* ) )> <!ELEMENT Over_Under ( Casino* )> <!ELEMENT Game ( ( GameID, Game_Date, Game_Time, AwayTeam, HomeTeam, Over_Under ) )> <!ELEMENT time_stamp (#PCDATA)>@@ -28,11 +28,12 @@ of anything in any order. We know that the first couple of elements are required and appear in a fixed order. -->-<!---Warning: the TSN DTD suggests the existence of an optional League_Name-element that would appear next to the Notes*.--->-<!ELEMENT message ( XML_File_ID, heading, category, sport, Title, Line_Time, ( Notes*, Game+)*, time_stamp )>+<!ELEMENT message ( XML_File_ID, heading, category, sport, Title, Line_Time, ( League_Name?, Notes*, Game+)+, time_stamp )>+<!ELEMENT League_Name (#PCDATA)>+<!ELEMENT AStarter (#PCDATA)>+<!ELEMENT HStarter (#PCDATA)>  <!ATTLIST Casino ClientID CDATA #REQUIRED> <!ATTLIST Casino Name CDATA #REQUIRED>+<!ATTLIST AStarter ID CDATA #REQUIRED>+<!ATTLIST HStarter ID CDATA #REQUIRED>
test/xml/earlylineXML.dtd view
@@ -9,7 +9,7 @@ <!ELEMENT teamH (#PCDATA)> <!ELEMENT over_under (#PCDATA)> <!ELEMENT game ( ( time, teamA, teamH, over_under ) )>-<!ELEMENT date ( ( note, game ) )>+<!ELEMENT date ( ( note?, game )* )> <!ELEMENT time_stamp (#PCDATA)> <!ELEMENT message ( ( XML_File_ID, heading, category, sport, title, date*, time_stamp ) )> 
test/xml/scoresxml.dtd view
@@ -2,26 +2,28 @@ <!ELEMENT heading (#PCDATA)> <!ELEMENT game_id (#PCDATA)> <!ELEMENT schedule_id (#PCDATA)>-<!ELEMENT tsnupdate (#PCDATA)> <!ELEMENT category (#PCDATA)> <!ELEMENT sport (#PCDATA)>+<!ELEMENT city (#PCDATA)>+<!ELEMENT state (#PCDATA)>+<!ELEMENT country (#PCDATA)>+<!ELEMENT location ( ( city, state, country ) )> <!ELEMENT seasontype (#PCDATA)> <!ELEMENT vteam (#PCDATA)> <!ELEMENT hteam (#PCDATA)> <!ELEMENT vscore (#PCDATA)> <!ELEMENT hscore (#PCDATA)>-<!ELEMENT time_r (#PCDATA)> <!ELEMENT status (#PCDATA)>+<!ELEMENT notes (#PCDATA)> <!ELEMENT game ( ( vteam, hteam, vscore, hscore, time_r?, status, notes? ) )> <!ELEMENT time_stamp (#PCDATA)> <!ELEMENT message ( ( XML_File_ID, heading, game_id, schedule_id, tsnupdate?, category, sport, location*, seasontype, game, time_stamp ) )>-<!ELEMENT city (#PCDATA)>-<!ELEMENT state (#PCDATA)>-<!ELEMENT country (#PCDATA)>-<!ELEMENT location ( ( city, state, country ) )>-<!ELEMENT notes (#PCDATA)>+<!ELEMENT time_r (#PCDATA)>+<!ELEMENT tsnupdate (#PCDATA)>  <!ATTLIST vteam id CDATA #REQUIRED>+<!ATTLIST vteam pitcher CDATA #IMPLIED> <!ATTLIST hteam id CDATA #REQUIRED>+<!ATTLIST hteam pitcher CDATA #IMPLIED> <!ATTLIST status numeral CDATA #REQUIRED>-<!ATTLIST status type CDATA #REQUIRED>+<!ATTLIST status type CDATA #IMPLIED>
test/xml/sportinfo/nflstandxml.dtd view
@@ -6,7 +6,7 @@ <!ELEMENT team (#PCDATA)> <!ELEMENT won (#PCDATA)> <!ELEMENT lost (#PCDATA)>-<!ELEMENT tied EMPTY>+<!ELEMENT tied (#PCDATA)> <!ELEMENT percentage (#PCDATA)> <!ELEMENT PF (#PCDATA)> <!ELEMENT PA (#PCDATA)>