htsn-import 0.2.1 → 0.2.2
raw patch · 21 files changed
+165/−72 lines, 21 files
Files
- doc/TODO +8/−11
- htsn-import.cabal +2/−1
- schema/earlylineXML.dtd +1/−1
- schema/nflstandxml.dtd +1/−1
- schemagen/earlylineXML/22064237.xml +1/−0
- src/Main.hs +27/−8
- src/Misc.hs +24/−0
- src/TSN/DbImport.hs +1/−2
- src/TSN/Picklers.hs +20/−0
- src/TSN/XML/EarlyLine.hs +3/−5
- src/TSN/XML/JFile.hs +9/−4
- src/TSN/XML/News.hs +4/−4
- src/TSN/XML/Odds.hs +18/−4
- src/TSN/XML/Scores.hs +5/−9
- src/TSN/XML/Weather.hs +3/−3
- src/Xml.hs +17/−2
- test/Doctests.hs +1/−0
- test/xml/Odds_XML.dtd +8/−7
- test/xml/earlylineXML.dtd +1/−1
- test/xml/scoresxml.dtd +10/−8
- test/xml/sportinfo/nflstandxml.dtd +1/−1
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)>