/// <summary> /// test if Energiemenge has only one <see cref="Mengeneinheit"/> /// </summary> /// <param name="em">Energiemenge</param> /// <returns>true iff the Energiemenge->energieverbrauch list does only contain entries with mutually convertible units</returns> public static bool IsPureMengeneinheit(this BO4E.BO.Energiemenge em) { using (MiniProfiler.Current.Step(nameof(IsPureMengeneinheit))) { ISet <Mengeneinheit> einheiten = new HashSet <Mengeneinheit>(); em.Energieverbrauch.All <Verbrauch>(v => einheiten.Add(v.Einheit)); if (einheiten.Count <= 1) { return(true); } else { Mengeneinheit me1 = einheiten.First <Mengeneinheit>(); foreach (Mengeneinheit me2 in einheiten) { if (!me1.IsConvertibleTo(me2)) { return(false); } } return(true); } } }
/// <summary> /// returns the factor that is needed to convert an amount in unit <paramref name="me1" /> to an equivalent amount in /// unit <paramref name="me2" />. /// </summary> /// <param name="me1">source unit</param> /// <param name="me2">target unit</param> /// <returns></returns> /// <exception cref="InvalidOperationException">iff units do not have the same dimension</exception> public static decimal GetConversionFactor(this Mengeneinheit me1, Mengeneinheit me2) { #pragma warning disable 618 if (me1 == Mengeneinheit.ZERO || me2 == Mengeneinheit.ZERO) #pragma warning restore 618 { throw new InvalidOperationException("You must not use the artificial 'ZERO' value."); } if (me1 == me2) { return(1.0M); } if (!me1.IsConvertibleTo(me2)) { throw new InvalidOperationException( $"{me1} and {me2} are not convertible into each other because they don't share the same dimension."); } if ((int)me1 % (int)me2 == 0 || (int)me2 % (int)me2 == 0) { return((decimal)me1 / (decimal)me2); } throw new InvalidOperationException($"{me1} and {me2} are not (trivially) convertible into each other."); }
/// <summary> /// convert to another unit if possible /// </summary> /// <param name="v">Verbrauch</param> /// <param name="mengeneinheit">Mengeneinheit</param> /// <throws>ArgumentException if units are not convertible</throws> public static void ConvertToUnit(this Verbrauch v, Mengeneinheit mengeneinheit) { PhysikalischerWert oldWert = new PhysikalischerWert(v.Wert, v.Einheit); PhysikalischerWert newWert = oldWert.ConvertToUnit(mengeneinheit); v.Wert = newWert.Wert; v.Einheit = newWert.Einheit; }
/// <summary> /// Tests if two Mengeneinheit are convertible into each other / do have the same kind. /// </summary> /// <param name="me1"></param> /// <param name="me2"></param> /// <returns>true iff convertible</returns> public static bool AreConvertible(Mengeneinheit me1, Mengeneinheit me2) { #pragma warning disable 618 if (me1 == Mengeneinheit.ZERO || me2 == Mengeneinheit.ZERO) { return(false); } #pragma warning restore 618 return(DimensionSets.Any(einheitengroup => einheitengroup.Contains(me1) && einheitengroup.Contains(me2))); }
/// <summary> /// Tests if two Mengeneinheit are convertible into each other / do have the same kind. /// </summary> /// <param name="me1"></param> /// <param name="me2"></param> /// <returns>true iff convertible</returns> public static bool AreConvertible(Mengeneinheit me1, Mengeneinheit me2) { foreach (ISet <Mengeneinheit> einheitengroup in DIMENSION_SETS) { if (einheitengroup.Contains(me1) && einheitengroup.Contains(me2)) { return(true); } } return(false); }
/// <summary> /// initialise with wert and string for einheit /// </summary> /// <param name="wert">numerischer wert</param> /// <param name="einheitString">zugehörige Einheit als string (case insensitive)</param> public PhysikalischerWert(decimal wert, string einheitString) : this() { this.Wert = wert; if (!Enum.TryParse <Mengeneinheit>(einheitString, true, out Mengeneinheit einheit)) { throw new ArgumentException($"'{einheitString}' is not a valid Mengeneinheit"); } else { this.Einheit = einheit; } }
private static HashSet <TimeSpan> GetTimeSpans(this BO.Energiemenge em, Wertermittlungsverfahren wev, string obis, Mengeneinheit me) { var result = new HashSet <TimeSpan>(); var vlist = new List <Verbrauch>(em.Energieverbrauch); vlist.Sort(new VerbrauchDateTimeComparer()); vlist = vlist.Where(v => v.Wertermittlungsverfahren == wev && v.Obiskennzahl == obis && v.Einheit == me) .ToList(); for (var i = 1; i < vlist.Count; i++) { result.Add(vlist[i].Startdatum - vlist[i - 1].Startdatum); result.Add(vlist[i].Enddatum - vlist[i - 1].Enddatum); } return(result); }
/// <summary> /// returns the factor that is needed to convert an amount in unit <paramref name="me1"/> to an equivalent amount in unit <paramref name="me2"/>. /// </summary> /// <param name="me1">source unit</param> /// <param name="me2">target unit</param> /// <returns></returns> /// <exception cref="InvalidOperationException">iff units do not have the same dimension</exception> public static decimal GetConversionFactor(this Mengeneinheit me1, Mengeneinheit me2) { if (me1 == me2) { return(1.0M); } if (!me1.IsConvertibleTo(me2)) { throw new InvalidOperationException($"{me1} and {me2} are not convertible into each other because they don't share the same dimension."); } if ((int)me1 % (int)me2 == 0 || (int)me2 % (int)me2 == 0) { return((decimal)me1 / (decimal)me2); } else { throw new InvalidOperationException($"{me1} and {me2} are not (trivially) convertible into each other."); } }
/// <summary> /// Returns the load in an intensive unit for a given point in time. /// </summary> /// <param name="em">Energiemenge</param> /// <param name="me">an intensive unit (e.g. "kW")</param> /// <param name="dt">point in time</param> /// <returns>load if Energiemenge BO contains value for specified date time<paramref name="dt"/>, null otherwise</returns> public static decimal?GetLoad(this BO4E.BO.Energiemenge em, Mengeneinheit me, DateTime dt) { if (!me.IsIntensive()) { throw new ArgumentException($"The Mengeneinheit {me} isn't intensive. Calculating the value for a specific point in time doesn't make sense."); } decimal?result = null; foreach (Verbrauch v in em.Energieverbrauch.Where(v => v.Startdatum <= dt && dt < v.Enddatum)) { if (result.HasValue) { result += v.Wert; } else { result = v.Wert; } } return(result); }
/// <summary> /// Test, if the single entries/intervals of the energieverbrauch array share the same duration and spacing in time. /// </summary> /// <param name="em">Energiemenge</param> /// <param name="reference">reference time frame</param> /// <param name="wev">Wertermittlungsverfahren</param> /// <param name="obis">OBIS-Kennzahl</param> /// <param name="me">Mengeneinheit</param> /// <param name="allowGaps">set true to allow gaps</param> /// <returns> /// True, if all energieverbrauch entries have the same length and their start and enddatum are evenly spaced. /// Also true, if there less than 2 entries in the energieverbrauch array. /// </returns> public static bool IsEvenlySpaced(this BO.Energiemenge em, ITimeRange reference, Wertermittlungsverfahren wev, string obis, Mengeneinheit me, bool allowGaps = false) { HashSet <TimeSpan> startEndDatumPeriods; using (MiniProfiler.Current.Step("finding time spans")) { startEndDatumPeriods = GetTimeSpans(em, wev, obis, me); } if (startEndDatumPeriods.Count < 2) { return(true); } if (allowGaps) { // each time difference must be a multiple of the smallest difference. using (MiniProfiler.Current.Step("Iterating over all time spans")) { var minDiff = startEndDatumPeriods.Min().TotalSeconds; foreach (var ts in startEndDatumPeriods) { if (Math.Abs(ts.TotalSeconds % minDiff) != 0) { // use profiler as logger: using (MiniProfiler.Current.Step( $"Found TimeSpan {ts} with a duration of {ts.TotalSeconds}. This is no multiple of {minDiff} => not evenly spaced.")) { return(false); } } } } return(true); } // there must be only 1 time difference between all the elements return(startEndDatumPeriods.Count <= 1); }
/// <summary> /// is the Mengeneinheit extensive? /// </summary> /// <param name="me"></param> /// <returns>true iff extensive</returns> public static bool IsExtensive(this Mengeneinheit me) { switch (me) { case Mengeneinheit.ANZAHL: return(true); case Mengeneinheit.JAHR: return(true); case Mengeneinheit.KUBIKMETER: return(true); case Mengeneinheit.KW: return(false); case Mengeneinheit.KWH: return(true); case Mengeneinheit.MONAT: return(true); case Mengeneinheit.MWH: return(true); case Mengeneinheit.STUNDE: return(true); case Mengeneinheit.TAG: return(true); case Mengeneinheit.WH: return(true); default: return(false); } }
/// <summary> /// Same as <see cref="GetAverage(Mengeneinheit, DateTime, DateTime)"/> but without specifying a time slice. /// </summary> /// <param name="em">Energiemenge</param> /// <param name="wev">type of measurement</param> /// <param name="obiskennzahl">OBIS</param> /// <param name="me">an intensive or extensive unit</param> /// <returns>The average for the given Mengeneinheit for the Energiemenge object or null if there was no Verbrauch for the given Mengeneinheit.</returns> public static decimal?GetAverage(this BO4E.BO.Energiemenge em, Wertermittlungsverfahren wev, string obiskennzahl, Mengeneinheit me) { return(em.GetAverage(em.GetTimeRange(), wev, obiskennzahl, me)); }
/// <summary> /// initialise with wert and einheit /// </summary> /// <param name="wert">numerischer Wert</param> /// <param name="einheit">zugehörige Mengeneinheit</param> public PhysikalischerWert(decimal wert, Mengeneinheit einheit) : this() { this.Wert = wert; this.Einheit = einheit; }
/// <summary> /// Converts a PhysikalischerWert to another unit, e.g. from kWh to MWh. This changes the <see cref="PhysikalischerWert.Einheit"/> and the <see cref="PhysikalischerWert.Wert"/> accordingly /// </summary> /// <param name="pw">physikalischer Wert</param> /// <param name="newEinheit">new unit of measurement</param> /// <returns>a new instance of PhysikalischerWert having the unit <paramref name="newEinheit"/></returns> public static PhysikalischerWert ConvertToUnit(this PhysikalischerWert pw, Mengeneinheit newEinheit) { decimal factor = pw.Einheit.GetConversionFactor(newEinheit); // throws all the exceptions. return(new PhysikalischerWert(factor * pw.Wert, newEinheit)); }
/// <summary> /// this constructor creates a BO4E.Rechnung from a JSON serialized SAP print document ("Druckbeleg") /// </summary> /// <param name="sapPrintDocument">a JSON serialized SAP print document using lowerCamelCase naming convention</param> public Rechnung(JObject sapPrintDocument) : this() { // why is this method so bloated and always tries to access two different keys of the JSON document using the ?? operator? // Initially I exported the SAP print document "Druckbeleg") using the SAP library /ui2/cl_json which allows for pretty printing // the ALL_UPPER_CASE SAP internal keys to lowerCamelCase. Later on technical constraints in SAP forced me to use a different // serialization which is closer to SAPs internal structure and has no lower case keys at all. Furthermore in SAP there is // no difference between string.Empty and null; the latter doesn't even exist as a concept. JToken infoToken = sapPrintDocument.SelectToken("erdk") ?? sapPrintDocument.SelectToken("ERDK"); JToken tErdzToken = sapPrintDocument.SelectToken("tErdz") ?? sapPrintDocument.SelectToken("T_ERDZ"); if (tErdzToken == null) { throw new ArgumentException("The SAP print document did not contain a 'tErdz' token. Did you serialize using the right naming convention?"); } Rechnungsnummer = (infoToken["opbel"] ?? infoToken["OPBEL"]).Value <string>(); Rechnungsdatum = new DateTimeOffset(TimeZoneInfo.ConvertTime((infoToken["bldat"] ?? infoToken["BLDAT"]).Value <DateTime>(), CentralEuropeStandardTime.CENTRAL_EUROPE_STANDARD_TIME, TimeZoneInfo.Utc)); Rechnungsperiode = new Zeitraum() { Startdatum = new DateTimeOffset(TimeZoneInfo.ConvertTime((tErdzToken[0]["ab"] ?? tErdzToken[0]["AB"]).Value <DateTime>(), CentralEuropeStandardTime.CENTRAL_EUROPE_STANDARD_TIME, TimeZoneInfo.Utc)), Enddatum = new DateTimeOffset(TimeZoneInfo.ConvertTime((tErdzToken[0]["bis"] ?? tErdzToken[0]["BIS"]).Value <DateTime>(), CentralEuropeStandardTime.CENTRAL_EUROPE_STANDARD_TIME, TimeZoneInfo.Utc)) }; Faelligkeitsdatum = new DateTimeOffset(TimeZoneInfo.ConvertTime((infoToken["faedn"] ?? infoToken["FAEDN"]).Value <DateTime>(), CentralEuropeStandardTime.CENTRAL_EUROPE_STANDARD_TIME, TimeZoneInfo.Utc)); Storno = false; decimal gSteure, gBrutto, vGezahlt, rBrutto; var gNetto = gSteure = _ = vGezahlt = rBrutto = 0.00M; Waehrungscode waehrungscode = (Waehrungscode)Enum.Parse(typeof(Waehrungscode), (infoToken["totalWaer"] ?? infoToken["TOTAL_WAER"]).Value <string>()); Waehrungseinheit waehrungseinheit = (Waehrungseinheit)Enum.Parse(typeof(Waehrungseinheit), (infoToken["totalWaer"] ?? infoToken["TOTAL_WAER"]).Value <string>()); Mengeneinheit mengeneinheit = (Mengeneinheit)Enum.Parse(typeof(Mengeneinheit), (tErdzToken[0]["massbill"] ?? tErdzToken[0]["MASSBILL"]).Value <string>()); List <Rechnungsposition> rpList = new List <Rechnungsposition>(); List <Steuerbetrag> stList = new List <Steuerbetrag>(); Vorausgezahlt = new Betrag() { Waehrung = waehrungscode, Wert = 0 }; foreach (JToken jrp in tErdzToken) { string belzart = (jrp["belzart"] ?? jrp["BELZART"]).ToString(); if (belzart == "IQUANT" || belzart == "ROUND" || belzart == "ROUNDO") { continue; } else { Rechnungsposition rp = new Rechnungsposition(); decimal zeitbezogeneMengeWert = 0; if (belzart == "000001") { rp.Positionstext = "ARBEITSPREIS"; } else if (belzart == "000003") { rp.Positionstext = "PAUSCHALE"; mengeneinheit = Mengeneinheit.JAHR; zeitbezogeneMengeWert = (jrp["preisbtr"] ?? jrp["PREISBTR"]).Value <decimal>(); rp.ZeitbezogeneMenge = new Menge() { Einheit = Mengeneinheit.TAG, Wert = zeitbezogeneMengeWert }; rp.Einzelpreis = new Preis() { Wert = decimal.Parse((jrp["zeitant"] ?? jrp["ZEITANT"]).ToString()), Einheit = waehrungseinheit, Bezugswert = mengeneinheit }; } else if (belzart == "000004") { rp.Positionstext = "VERRECHNUNGSPREIS"; } else if (belzart == "SUBT") { rp.Positionstext = "zuzüglich Mehrwertsteuer 19,000%"; } else if (belzart == "ZHFBP1" || belzart == "CITAX") { rp.Positionstext = belzart; } else { rp.Positionstext = ""; } if ((jrp["massbill"] ?? jrp["MASSBILL"]) != null && !string.IsNullOrWhiteSpace((jrp["massbill"] ?? jrp["MASSBILL"]).Value <string>())) { mengeneinheit = (Mengeneinheit)Enum.Parse(typeof(Mengeneinheit), (jrp["massbill"] ?? jrp["MASSBILL"]).Value <string>()); } else if ((jrp["timbasis"] ?? jrp["TIMBASIS"]) != null && !string.IsNullOrWhiteSpace((jrp["timbasis"] ?? jrp["TIMBASIS"]).Value <string>())) { if ((jrp["timbasis"] ?? jrp["TIMBASIS"]).Value <string>() == "365") { mengeneinheit = Mengeneinheit.JAHR; rp.ZeitbezogeneMenge = new Menge() { Einheit = Mengeneinheit.TAG, Wert = zeitbezogeneMengeWert }; } } else { mengeneinheit = Mengeneinheit.KWH; } if (rp.Einzelpreis == null) { if ((jrp["preisbtr"] ?? jrp["PREISBTR"]) != null) { rp.Einzelpreis = new Preis() { Wert = decimal.Parse((jrp["preisbtr"] ?? jrp["PREISBTR"]).ToString()), Einheit = waehrungseinheit, Bezugswert = mengeneinheit }; } else { rp.Einzelpreis = new Preis() { Wert = 0, Einheit = waehrungseinheit, Bezugswert = mengeneinheit } }; } rp.Positionsnummer = (jrp["belzeile"] ?? jrp["BELZEILE"]).Value <int>(); if ((jrp["bis"] ?? jrp["BIS"]) != null && (jrp["bis"] ?? jrp["BIS"]).Value <string>() != "0000-00-00") { rp.LieferungBis = new DateTimeOffset(TimeZoneInfo.ConvertTime((jrp["bis"] ?? jrp["BIS"]).Value <DateTime>(), CentralEuropeStandardTime.CENTRAL_EUROPE_STANDARD_TIME, TimeZoneInfo.Utc)); } if ((jrp["ab"] ?? jrp["AB"]) != null && (jrp["ab"] ?? jrp["AB"]).Value <string>() != "0000-00-00") { rp.LieferungVon = new DateTimeOffset(TimeZoneInfo.ConvertTime((jrp["ab"] ?? jrp["AB"]).Value <DateTime>(), CentralEuropeStandardTime.CENTRAL_EUROPE_STANDARD_TIME, TimeZoneInfo.Utc)); } if ((jrp["vertrag"] ?? jrp["VERTRAG"]) != null) { #pragma warning disable CS0618 // Type or member is obsolete rp.VertragskontoId = (jrp["vertrag"] ?? jrp["VERTRAG"]).Value <string>(); #pragma warning restore CS0618 // Type or member is obsolete } if ((jrp["iAbrmenge"] ?? jrp["I_ABRMENGE"]) != null) { rp.PositionsMenge = new Menge() { Wert = (jrp["iAbrmenge"] ?? jrp["I_ABRMENGE"]).Value <decimal>(), Einheit = mengeneinheit }; } if ((jrp["nettobtr"] ?? jrp["NETTOBTR"]) != null) { if (belzart != "SUBT" && belzart != "CITAX") { rp.TeilsummeNetto = new Betrag() { Wert = (jrp["nettobtr"] ?? jrp["NETTOBTR"]).Value <decimal>(), Waehrung = waehrungscode }; } else { rp.TeilsummeNetto = new Betrag() { Wert = (jrp["sbasw"] ?? jrp["SBASW"]).Value <decimal>(), Waehrung = waehrungscode }; Steuerbetrag steuerbetrag = new Steuerbetrag() { Basiswert = (jrp["sbasw"] ?? jrp["SBASW"]).Value <decimal>(), Steuerwert = (jrp["sbetw"] ?? jrp["SBETW"]).Value <decimal>(), Waehrung = (Waehrungscode)Enum.Parse(typeof(Waehrungscode), (jrp["twaers"] ?? jrp["TWAERS"]).Value <string>()) }; decimal steuerProzent; if ((jrp["stprz"] ?? jrp["STPRZ"]) != null && !string.IsNullOrWhiteSpace((jrp["stprz"] ?? jrp["STPRZ"]).Value <string>())) { steuerProzent = decimal.Parse((jrp["stprz"] ?? jrp["STPRZ"]).Value <string>().Replace(",", ".").Trim(), CultureInfo.InvariantCulture); } else { steuerProzent = steuerbetrag.Steuerwert / steuerbetrag.Basiswert * 100.0M; } if ((int)steuerProzent == 19) { steuerbetrag.Steuerkennzeichen = Steuerkennzeichen.UST_19; } else if ((int)steuerProzent == 7) { steuerbetrag.Steuerkennzeichen = Steuerkennzeichen.UST_7; } else { throw new NotImplementedException($"Taxrate Internal '{jrp["taxrateInternal"]}' is not mapped."); } rp.TeilsummeSteuer = steuerbetrag; } if ((jrp["nettobtr"] ?? jrp["NETTOBTR"]).Value <decimal>() <= 0) { Vorausgezahlt = new Betrag() { Waehrung = waehrungscode, Wert = (jrp["nettobtr"] ?? jrp["NETTOBTR"]).Value <decimal>() }; } } rp.Zeiteinheit = mengeneinheit; rpList.Add(rp); var be = (jrp["nettobtr"] ?? jrp["NETTOBTR"]); if (be != null) { if (belzart != "SUBT" && belzart != "CITAX") // this will lead to problems in the long run. { gNetto += be.Value <decimal>(); } else { Steuerbetrag steuerbetrag = new Steuerbetrag() { Basiswert = (jrp["sbasw"] ?? jrp["SBASW"]).Value <decimal>(), Steuerwert = (jrp["sbetw"] ?? jrp["SBETW"]).Value <decimal>(), Waehrung = (Waehrungscode)Enum.Parse(typeof(Waehrungscode), (jrp["twaers"] ?? jrp["TWAERS"]).Value <string>()) }; decimal steuerProzent; if ((jrp["stprz"] ?? jrp["STPRZ"]) != null && !string.IsNullOrWhiteSpace((jrp["stprz"] ?? jrp["STPRZ"]).Value <string>())) { steuerProzent = decimal.Parse((jrp["stprz"] ?? jrp["STPRZ"]).Value <string>().Replace(",", ".").Trim(), CultureInfo.InvariantCulture); } else { steuerProzent = Math.Round(steuerbetrag.Steuerwert / steuerbetrag.Basiswert * 100.0M); } if (steuerProzent == 19.0M) { steuerbetrag.Steuerkennzeichen = Steuerkennzeichen.UST_19; } else if (steuerProzent == 7.0M) { steuerbetrag.Steuerkennzeichen = Steuerkennzeichen.UST_7; } else { throw new NotImplementedException($"Taxrate Internal '{jrp["taxrateInternal"] ?? jrp["TAXRATE_INTERNAL"]}' is not mapped."); } stList.Add(steuerbetrag); gSteure += be.Value <decimal>(); } } } } Steuerbetraege = stList; Rechnungspositionen = rpList; gBrutto = gNetto + gSteure; var zZahlen = gBrutto - vGezahlt - rBrutto; Gesamtnetto = new Betrag() { Wert = gNetto, Waehrung = waehrungscode }; Gesamtsteuer = new Betrag() { Wert = gSteure, Waehrung = waehrungscode }; Gesamtbrutto = new Betrag() { Wert = gBrutto, Waehrung = waehrungscode }; Zuzahlen = new Betrag() { Wert = zZahlen, Waehrung = waehrungscode }; Rechnungsersteller = new Geschaeftspartner() { Geschaeftspartnerrolle = new List <Geschaeftspartnerrolle>() { Geschaeftspartnerrolle.LIEFERANT }, Gewerbekennzeichnung = true, Anrede = Anrede.HERR, Name1 = "Mein super Lieferant", Partneradresse = new Adresse() { Strasse = "Max-Plank-Strasse", Hausnummer = "8", Postleitzahl = "90190", Landescode = Landescode.DE, Ort = "Walldorf" } }; Rechnungsempfaenger = new Geschaeftspartner() { Geschaeftspartnerrolle = new List <Geschaeftspartnerrolle>() { Geschaeftspartnerrolle.KUNDE }, Gewerbekennzeichnung = false, Anrede = Anrede.HERR, Name1 = "Lustig", Name2 = "Peter", Partneradresse = new Adresse() { Strasse = "Magnusstraße", Hausnummer = "20", Postleitzahl = "50672", Landescode = Landescode.DE, Ort = "Köln" } }; } }
/// <summary> /// Generate a <see cref="CompletenessReport"/> for the given parameters. /// </summary> /// <param name="em">Energiemenge</param> /// <param name="reference">reference time frame</param> /// <param name="wev">Wertermittlungsverfahren</param> /// <param name="obiskennzahl">OBIS Kennzahl</param> /// <param name="einheit">Mengeneinheit</param> /// <returns>the completeness report</returns> public static CompletenessReport GetCompletenessReport(this BO4E.BO.Energiemenge em, ITimeRange reference, Wertermittlungsverfahren wev, string obiskennzahl, Mengeneinheit einheit) { CompletenessReport result; using (MiniProfiler.Current.Step("create completeness report skeleton + find the coverage")) { result = new CompletenessReport { LokationsId = em.LokationsId, Einheit = einheit, Coverage = GetCoverage(em, reference, wev, obiskennzahl, einheit), wertermittlungsverfahren = wev, Obiskennzahl = obiskennzahl, ReferenceTimeFrame = new Zeitraum { Startdatum = DateTime.SpecifyKind(reference.Start, DateTimeKind.Utc), Enddatum = DateTime.SpecifyKind(reference.End, DateTimeKind.Utc) }, }; } if (em.Energieverbrauch != null && em.Energieverbrauch.Count > 0) { /*using (MiniProfiler.Current.Step("populating time slices of/with missing/null values")) * { * result.values = em.GetMissingTimeRanges(reference, wev, obis, einheit) * .Select(mtr => new CompletenessReport.BasicVerbrauch * { * startdatum = DateTime.SpecifyKind(mtr.Start, DateTimeKind.Utc), * enddatum = DateTime.SpecifyKind(mtr.End, DateTimeKind.Utc), * wert = null * }).ToList<CompletenessReport.BasicVerbrauch>(); * } * using (MiniProfiler.Current.Step("populating time slices existing values")) * { * result.values.AddRange( * em.energieverbrauch * //.AsParallel<Verbrauch>() * .Where(v => v.obiskennzahl == obis && v.einheit == einheit && v.wertermittlungsverfahren == wev) * .Select(v => new CompletenessReport.BasicVerbrauch * { * startdatum = DateTime.SpecifyKind(v.startdatum, DateTimeKind.Utc), * enddatum = DateTime.SpecifyKind(v.enddatum, DateTimeKind.Utc), * wert = v.wert * }) * .ToList<CompletenessReport.BasicVerbrauch>()); * }*/ using (MiniProfiler.Current.Step("Setting aggregated gaps")) { var nonNullValues = new TimePeriodCollection(em.Energieverbrauch.Select(v => new TimeRange(v.Startdatum, v.Enddatum))); ITimeRange limits; if (result.ReferenceTimeFrame != null && result.ReferenceTimeFrame.Startdatum.HasValue && result.ReferenceTimeFrame.Enddatum.HasValue) { limits = new TimeRange(result.ReferenceTimeFrame.Startdatum.Value, result.ReferenceTimeFrame.Enddatum.Value); } else { limits = null; } var gaps = (new TimeGapCalculator <TimeRange>()).GetGaps(nonNullValues, limits: limits); result.Gaps = gaps.Select(gap => new CompletenessReport.BasicVerbrauch() { Startdatum = gap.Start, Enddatum = gap.End, Wert = null }).ToList(); } /*using (MiniProfiler.Current.Step("sorting result")) * { * result.values.Sort(new BasicVerbrauchDateTimeComparer()); * }*/ if (em.IsPure(checkUserProperties: true)) { try { foreach (var kvp in em.Energieverbrauch.Where(v => v.UserProperties != null).SelectMany(v => v.UserProperties)) { if (result.UserProperties == null) { result.UserProperties = new Dictionary <string, JToken>(); } if (!result.UserProperties.ContainsKey(kvp.Key)) { result.UserProperties.Add(kvp.Key, kvp.Value); } } } catch (InvalidOperationException) { // ok, there's no Verbrauch with user properties. } } } /*else * { * result.coverage = null; * result._errorMessage = "energieverbrauch is empty"; * }*/ return(result); }
/// <summary> /// Returns the consumption of a given kind of Mengeneinheit within the specified reference time range. /// </summary> /// <param name="em">Energiemenge</param> /// <param name="reference">reference time frame</param> /// <param name="wev">Wertermittlungsverfahren</param> /// <param name="obiskennzahl">OBIS number</param> /// <param name="me">an extensive unit (e.g. "kWh")</param> /// <returns>the consumption within the give time slice <paramref name="reference"/> in the unit passed as <paramref name="me"/></returns> public static decimal GetConsumption(this BO4E.BO.Energiemenge em, ITimeRange reference, Wertermittlungsverfahren wev, string obiskennzahl, Mengeneinheit me) { if (!me.IsExtensive()) { throw new ArgumentException($"The Mengeneinheit {me} isn't extensive. Calculating a consumption doesn't make sense."); } return(em.Energieverbrauch .Where(v => v.Wertermittlungsverfahren == wev && v.Obiskennzahl == obiskennzahl && v.Einheit == me) //.AsParallel<Verbrauch>() .Sum(v => GetOverlapFactor(new TimeRange(v.Startdatum, v.Enddatum), reference, false) * v.Wert)); }
/// <summary> /// Get ratio of overlap between given Energiemenge and a reference. /// Method is basically just another name for <see cref="GetOverlapFactor(TimeRange, TimeRange, bool)"/> /// </summary> /// <param name="em">Energiemenge</param> /// <param name="reference">reference time range</param> /// <param name="obisKz">OBIS</param> /// <param name="mengeneinheit">unit of measurement</param> /// <param name="wev">type of measurement</param> /// <param name="decimalRounding">post decimals</param> /// <returns>value between 0 (no overlap) and 1.0 (100% overlap)</returns> public static decimal GetCoverage(this BO4E.BO.Energiemenge em, ITimeRange reference, Wertermittlungsverfahren wev, string obisKz, Mengeneinheit mengeneinheit, int decimalRounding = 10) { decimal exactResult; using (MiniProfiler.Current.Step($"calculating coverage for list with {em.Energieverbrauch.Count} entries.")) { exactResult = em.Energieverbrauch //.AsParallel<Verbrauch>() .Where <Verbrauch>(v => v.Einheit == mengeneinheit && v.Obiskennzahl == obisKz && v.Wertermittlungsverfahren == wev) .Sum(v => GetOverlapFactor(new TimeRange(v.Startdatum, v.Enddatum), reference, true)); } return(Math.Round(exactResult, decimalRounding)); }
/// <summary> /// Is the Mengeneinheit intensive? /// </summary> /// <param name="me"></param> /// <returns>true iff not extensive</returns> public static bool IsIntensive(this Mengeneinheit me) { return(!IsExtensive(me)); }
/// <summary> /// Similar to AreConvertible but as extension method for me1 /// </summary> /// <param name="me1"></param> /// <param name="me2"></param> /// <returns></returns> public static bool IsConvertibleTo(this Mengeneinheit me1, Mengeneinheit me2) { return(AreConvertible(me1, me2)); }
/// <summary> /// Get average of Mengeneinheit for given time interval /// </summary> /// <param name="em">Energiemenge</param> /// <param name="reference">reference time frame</param> /// <param name="wev">Wertermittlungsverfahren</param> /// <param name="obiskennzahl">OBIS</param> /// <param name="me">an extensive or intensive unit</param> /// <returns>the average value or null if no Verbrauch overlapped with the specified time interval</returns> public static decimal?GetAverage(this BO4E.BO.Energiemenge em, TimeRange reference, Wertermittlungsverfahren wev, string obiskennzahl, Mengeneinheit me) { decimal?result = null; decimal overallDenominator = 0.0M; foreach (Verbrauch v in em.Energieverbrauch.Where(v => v.Einheit == me)) { decimal overlapFactor = GetOverlapFactor(new TimeRange(v.Startdatum, v.Enddatum), reference, true); if (result.HasValue) { result += overlapFactor * v.Wert; } else { result = v.Wert; } overallDenominator += overlapFactor; } if (result.HasValue) { return(result / overallDenominator); } else { return(result); } }
/// <summary> /// Get a list of those time ranges within a reference, where no energieverbrauch entries are defined. /// </summary> /// <param name="em">Energiemenge</param> /// <param name="reference">reference timeframe</param> /// <param name="wev">Wertermittlungsverfahren</param> /// <param name="obis">OBIS-Kennzahl</param> /// <param name="me">Mengeneinheit</param> /// <returns></returns> public static List <TimeRange> GetMissingTimeRanges(this BO4E.BO.Energiemenge em, ITimeRange reference, Wertermittlungsverfahren wev, string obis, Mengeneinheit me) { using (MiniProfiler.Current.Step(nameof(GetMissingTimeRanges))) { IDictionary <Tuple <DateTime, DateTime>, Verbrauch> filteredVerbrauch; using (MiniProfiler.Current.Step($"Filtering energieverbrauch on OBIS={obis}, WEV={wev}, Mengeneinheit={me}")) { filteredVerbrauch = em.Energieverbrauch .Where <Verbrauch>(v => v.Wertermittlungsverfahren == wev && v.Obiskennzahl == obis && v.Einheit == me) .ToDictionary(v => new Tuple <DateTime, DateTime>(v.Startdatum, v.Enddatum), v => v); } if (filteredVerbrauch.Count < 2) { throw new ArgumentException("Not enough entries in energieverbrauch to determine periodicity."); } if (!IsEvenlySpaced(em, reference, wev, obis, me, true)) { throw new ArgumentException("The provided Energiemenge is not evenly spaced although gaps are allowed."); } TimeSpan periodicity = GetTimeSpans(em, wev, obis, me).Min <TimeSpan>(); if (Math.Abs((reference.Start - em.GetMinDate()).TotalMilliseconds % periodicity.TotalMilliseconds) != 0) { throw new ArgumentException($"The absolute difference between reference.start ({reference.Start}) and the minimal date time in the Energiemenge ({em.GetMinDate()}) has to be an integer multiple of the periodicity {periodicity.TotalMilliseconds} but was {(reference.Start - em.GetMinDate()).TotalMilliseconds}."); } // since it's assured, that the energieverbrauch entries are evenly spaced it doesn't matter which entry we use to determine the duration. TimeSpan duration = filteredVerbrauch.Values.Min(v => v.Enddatum) - filteredVerbrauch.Values.Min(v => v.Startdatum); List <TimeRange> result = new List <TimeRange>(); using (MiniProfiler.Current.Step("Populating list with time slices in UTC")) { for (DateTime dt = reference.Start; dt < reference.End; dt += periodicity) { // use a strict '==' instead of overlap. This is justified because all the other cases are considered beforehand switch (dt.Kind) { case DateTimeKind.Local: throw new ArgumentException("Local DateTime not supported!"); case DateTimeKind.Unspecified: dt = DateTime.SpecifyKind(dt, DateTimeKind.Utc); break; case DateTimeKind.Utc: break; } //using (MiniProfiler.Current.Step("linq where on filtered verbrauch")) //{ if (!filteredVerbrauch.ContainsKey(new Tuple <DateTime, DateTime>(dt, dt + duration)))// Where<Verbrauch>(v => v.startdatum == dt && v.enddatum == dt + duration).Any()) { result.Add(new TimeRange(dt, dt + duration)); } //} } } return(result); } }
/// <summary> /// Get total consumption for given parameters /// </summary> /// <param name="em">Energiemenge</param> /// <param name="wev">type of measurement</param> /// <param name="obiskennzahl">OBIS</param> /// <param name="me">unit of measurement</param> /// <returns>consumption value</returns> public static decimal GetTotalConsumption(this BO.Energiemenge em, Wertermittlungsverfahren wev, string obiskennzahl, Mengeneinheit me) { return(em.GetConsumption(em.GetTimeRange(), wev, obiskennzahl, me)); }