diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 02960c4e..d98c25e4 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -5,8 +5,7 @@ "csharpier": { "version": "0.29.2", "commands": [ - "dotnet-csharpier", - "reportgenerator" + "dotnet-csharpier" ], "rollForward": false }, @@ -25,4 +24,4 @@ "rollForward": false } } -} \ No newline at end of file +} diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 00000000..8ac796ce --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,68 @@ +name: Setup Flowthru CI Environment +description: > + Installs all non-Node toolchains (.NET, Node, Python, uv, pnpm, Java, Spark) + and runs pnpm install (which also runs dotnet restore via the postinstall hook). + +runs: + using: composite + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + dotnet-quality: "preview" + env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true" + DOTNET_NOLOGO: "true" + DOTNET_CLI_TELEMETRY_OPTOUT: "true" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Setup UV + uses: astral-sh/setup-uv@v5 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.6.3 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Setup Apache Spark 4.1.1 + shell: bash + run: | + SPARK_VERSION=4.1.1 + HADOOP_VERSION=3 + INSTALL_DIR=/opt/spark + + curl -fsSL \ + "https://archive.apache.org/dist/spark/spark-${SPARK_VERSION}/spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz" \ + | tar -xz -C /tmp + + sudo mv "/tmp/spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}" "${INSTALL_DIR}" + echo "SPARK_HOME=${INSTALL_DIR}" >> $GITHUB_ENV + echo "${INSTALL_DIR}/bin" >> $GITHUB_PATH + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Install Node dependencies + shell: bash + run: pnpm install diff --git a/.github/hooks/scripts/stop-build.js b/.github/hooks/scripts/stop-build.js index 49bcc36d..18974bb0 100755 --- a/.github/hooks/scripts/stop-build.js +++ b/.github/hooks/scripts/stop-build.js @@ -13,6 +13,72 @@ const { spawnSync } = require('child_process'); const path = require('path'); +/** + * Parse dotnet test output to extract test counts. + * Aggregates counts across all test runs. + * Returns: { passed, failed, skipped, inconclusive, total } + */ +function parseTestCounts(output) { + // Match patterns like: "Failed: 6, Passed: 156, Skipped: 0, Total: 162" + const regex = /Failed:\s*(\d+),\s*Passed:\s*(\d+),\s*Skipped:\s*(\d+),\s*Total:\s*(\d+)/g; + const counts = { passed: 0, failed: 0, skipped: 0, total: 0 }; + + let match; + while ((match = regex.exec(output)) !== null) { + counts.failed += parseInt(match[1], 10); + counts.passed += parseInt(match[2], 10); + counts.skipped += parseInt(match[3], 10); + counts.total += parseInt(match[4], 10); + } + + // Inconclusive = Total - Passed - Failed - Skipped + counts.inconclusive = counts.total - counts.passed - counts.failed - counts.skipped; + + return counts; +} + +/** + * Format test counts for display. + */ +function formatTestCounts(counts) { + return [ + `Passed: ${counts.passed}`, + `Failed: ${counts.failed}`, + `Skipped: ${counts.skipped}`, + `Inconclusive: ${counts.inconclusive}`, + `Total: ${counts.total}`, + ].join('\n'); +} + +/** + * Extract full failure blocks from dotnet test output. + * A block starts with an indented "Failed " line and continues + * until the next dotnet test summary line or end of output. + */ +function extractFailureBlocks(output) { + const lines = output.split('\n'); + const resultLines = []; + let capturing = false; + + for (const line of lines) { + if (/^\s+Failed\s+\S/.test(line)) { + // Start of a new failure block — blank separator between blocks. + if (resultLines.length > 0) resultLines.push(''); + capturing = true; + resultLines.push(line); + } else if (capturing) { + // Stop at the dotnet test run summary line (e.g. "Failed! - Failed: 6, Passed: ...") + if (/^(Failed!|Passed!)\s+-\s+Failed:/.test(line.trim())) { + capturing = false; + } else { + resultLines.push(line); + } + } + } + + return resultLines.join('\n').trim(); +} + // Read and parse stdin to detect re-entry. let hookInput = {}; try { @@ -66,14 +132,14 @@ const stdout = (result.stdout || '').trim(); const stderr = (result.stderr || '').trim(); const combined = [stdout, stderr].filter(Boolean).join('\n'); -if (result.status !== 0) { - // Extract failed test lines for a focused summary. - const failureLines = combined - .split('\n') - .filter(line => /failed|error|FAILED|ERROR/i.test(line)) - .slice(0, 40); // cap at 40 lines to avoid overwhelming the agent +// Parse test counts from output. +const testCounts = parseTestCounts(combined); +const countsDisplay = formatTestCounts(testCounts); - const summary = failureLines.length > 0 ? failureLines.join('\n') : combined; +// NX does not always propagate the dotnet exit code — fall back to parsed counts. +if (result.status !== 0 || testCounts.failed > 0) { + const failureBlocks = extractFailureBlocks(combined); + const summary = failureBlocks.length > 0 ? failureBlocks : combined; process.stdout.write(JSON.stringify({ hookSpecificOutput: { @@ -82,12 +148,21 @@ if (result.status !== 0) { reason: [ `nx affected test FAILED (affected: ${affectedProjects.join(', ')}) — address these failures before concluding.`, '', + 'Test Summary:', + countsDisplay, + '', summary, ].join('\n'), }, })); + process.exit(1); } else { process.stdout.write(JSON.stringify({ - systemMessage: `nx affected test: passed (${affectedProjects.length} project(s): ${affectedProjects.join(', ')}).`, + systemMessage: [ + `nx affected test: passed (${affectedProjects.length} project(s): ${affectedProjects.join(', ')}).`, + '', + 'Test Summary:', + countsDisplay, + ].join('\n'), })); } diff --git a/.github/instructions/flowthru.instructions.md b/.github/instructions/flowthru.instructions.md index 7b7b65e3..a695ef63 100644 --- a/.github/instructions/flowthru.instructions.md +++ b/.github/instructions/flowthru.instructions.md @@ -1,4 +1,5 @@ --- +description: Guidelines for routing Flowthru sessions based on development focus. applyTo: "**" --- diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 898d2091..a244b3d2 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -26,44 +26,8 @@ jobs: with: fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: "10.0.x" - dotnet-quality: "preview" - env: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true" - DOTNET_NOLOGO: "true" - DOTNET_CLI_TELEMETRY_OPTOUT: "true" - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Setup UV - uses: astral-sh/setup-uv@v5 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10.6.3 - - - name: Cache NuGet packages - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj') }} - restore-keys: | - ${{ runner.os }}-nuget- - - - name: Install Node dependencies - run: pnpm install + - name: Setup CI environment + uses: ./.github/actions/setup - name: Run affected tests env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 327bc4f3..7979329d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,47 +28,8 @@ jobs: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: "10.0.x" - dotnet-quality: "preview" - env: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true" - DOTNET_NOLOGO: "true" - DOTNET_CLI_TELEMETRY_OPTOUT: "true" - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Setup UV - uses: astral-sh/setup-uv@v5 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10.6.3 - - - name: Cache NuGet packages - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj') }} - restore-keys: | - ${{ runner.os }}-nuget- - - - name: Install Node dependencies - run: pnpm install - - - name: Install ReportGenerator - run: dotnet tool install --global dotnet-reportgenerator-globaltool + - name: Setup CI environment + uses: ./.github/actions/setup - name: Compute affected projects id: affected @@ -99,21 +60,86 @@ jobs: --settings coverlet.runsettings \ --logger "console;verbosity=minimal" - - name: Generate coverage badges - continue-on-error: true - env: - REPORTGENERATOR_LICENSE: ${{ secrets.REPORTGENERATOR_LICENSE }} - run: | - COVERAGE_FILES=$(find tests -name "coverage.cobertura.xml" 2>/dev/null | head -1) - if [ -n "$COVERAGE_FILES" ]; then - reportgenerator \ - "-reports:tests/**/TestResults/**/coverage.cobertura.xml" \ - "-targetdir:coverage-badges" \ - "-reporttypes:Badges" \ - "-verbosity:Warning" - else - echo "No coverage files found — skipping badge generation." - fi + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: core + files: tests/Flowthru.Tests/**/coverage.cobertura.xml + fail_ci_if_error: false + + - name: Upload coverage (funit) + if: always() + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: funit + files: tests/Flowthru.FUnit.Tests/**/coverage.cobertura.xml + fail_ci_if_error: false + + - name: Upload coverage (examples) + if: always() + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: examples + files: tests/Flowthru.Tests.Examples/**/coverage.cobertura.xml + fail_ci_if_error: false + + - name: Upload coverage (templates) + if: always() + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: templates + files: tests/Flowthru.Tests.Templates/**/coverage.cobertura.xml + fail_ci_if_error: false + + - name: Upload coverage (spark) + if: always() + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: spark + files: tests/Flowthru.Tests.Spark/**/coverage.cobertura.xml + fail_ci_if_error: false + + - name: Upload coverage (ext-efcore) + if: always() + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: ext-efcore + files: tests/Flowthru.Extensions.EFCore.Tests/**/coverage.cobertura.xml + fail_ci_if_error: false + + - name: Upload coverage (ext-gql) + if: always() + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: ext-gql + files: tests/Flowthru.Extensions.GQL.Tests/**/coverage.cobertura.xml + fail_ci_if_error: false + + - name: Upload coverage (ext-python) + if: always() + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: ext-python + files: tests/Flowthru.Extensions.Python.Tests/**/coverage.cobertura.xml + fail_ci_if_error: false + + - name: Upload coverage (ext-spark) + if: always() + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: ext-spark + files: tests/Flowthru.Extensions.Spark.Tests/**/coverage.cobertura.xml + fail_ci_if_error: false - name: Configure Git run: | @@ -246,11 +272,3 @@ jobs: } > /tmp/combined-notes.md gh release edit "${VERSION}" --notes-file /tmp/combined-notes.md fi - - - name: Upload coverage badges as artifact - if: always() - uses: actions/upload-artifact@v4 - with: - name: coverage-badges - path: coverage-badges/ - retention-days: 7 diff --git a/.gitignore b/.gitignore index 3d42d73b..f9f8e79b 100644 --- a/.gitignore +++ b/.gitignore @@ -285,3 +285,4 @@ coverage-badges/ __pycache__/ *.py[cod] *$py.class +Flowthru.sln.DotSettings.user diff --git a/Directory.Build.targets b/Directory.Build.targets index 00591d1c..ba8d04fb 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -53,6 +53,11 @@ + + + + + diff --git a/Flowthru.slnx b/Flowthru.slnx index 145058c5..c569ad6e 100644 --- a/Flowthru.slnx +++ b/Flowthru.slnx @@ -1,6 +1,9 @@ + + + @@ -19,6 +22,13 @@ + + + + + + + @@ -32,6 +42,9 @@ + + + diff --git a/README.md b/README.md index 5fdab844..88a9885e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Flowthru: We'll Never Fail Again +[![codecov](https://codecov.io/gh/chaoticgoodcomputing/flowthru/branch/main/graph/badge.svg)](https://codecov.io/gh/chaoticgoodcomputing/flowthru) + Flowthru is a data pipeline framework for .NET that promises a stable, fault-free data science and engineering process. The premise is simple: **A good pipeline will always finish. A broken pipeline will break fast.** diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES new file mode 100644 index 00000000..4e69431a --- /dev/null +++ b/THIRD-PARTY-NOTICES @@ -0,0 +1,36 @@ +THIRD-PARTY NOTICES + +Flowthru incorporates material from the following projects: + +------------------------------------------------------------------------------- + +dotnet/spark — .NET for Apache Spark +https://github.com/dotnet/spark + +Files: src/spark/Flowthru.Spark/, tests/Flowthru.Tests.Spark/ + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------- diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..ae55dc4a --- /dev/null +++ b/codecov.yml @@ -0,0 +1,46 @@ +codecov: + require_ci_to_pass: true + +coverage: + status: + project: + default: + target: auto + threshold: 1% + patch: + default: + target: 80% + +flags: + core: + paths: [] + carryforward: true + funit: + paths: [] + carryforward: true + examples: + paths: [] + carryforward: true + templates: + paths: [] + carryforward: true + spark: + paths: [] + carryforward: true + ext-efcore: + paths: [] + carryforward: true + ext-gql: + paths: [] + carryforward: true + ext-python: + paths: [] + carryforward: true + ext-spark: + paths: [] + carryforward: true + +comment: + layout: "reach,diff,flags,components" + behavior: default + require_changes: false diff --git a/docs/reference/misc/external/k8s-net/.source.env b/docs/reference/misc/external/k8s-net/.source.env new file mode 100644 index 00000000..d0f25834 --- /dev/null +++ b/docs/reference/misc/external/k8s-net/.source.env @@ -0,0 +1,2 @@ +SOURCE_TYPE=repo +SOURCE_ADDRESS=https://github.com/kubernetes-client/csharp.git diff --git a/docs/reference/misc/external/ms-referencesource/.source.env b/docs/reference/misc/external/ms-referencesource/.source.env new file mode 100644 index 00000000..efda61b0 --- /dev/null +++ b/docs/reference/misc/external/ms-referencesource/.source.env @@ -0,0 +1,2 @@ +SOURCE_TYPE=repo +SOURCE_ADDRESS=https://github.com/microsoft/referencesource.git diff --git a/docs/reference/misc/external/vstest/.source.env b/docs/reference/misc/external/vstest/.source.env new file mode 100644 index 00000000..f749b3f5 --- /dev/null +++ b/docs/reference/misc/external/vstest/.source.env @@ -0,0 +1,2 @@ +SOURCE_TYPE=repo +SOURCE_ADDRESS=https://github.com/microsoft/vstest.git diff --git a/examples/starter/KedroSpaceflightsSpark/.gitignore b/examples/starter/KedroSpaceflightsSpark/.gitignore new file mode 100644 index 00000000..4a3b2926 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/.gitignore @@ -0,0 +1,19 @@ +# Generated metadata +Metadata/ + +# Ignore all generated data artifacts +Data/** + +# Un-ignore layer directories and their Schemas subdirectories so git can traverse them +!Data/*/ +!Data/*/Schemas/ +!Data/_01_Raw/Datasets/ + +# Allow raw input dataset files +!Data/_01_Raw/Datasets/* + +# Allow all schema and catalog .cs files +!Data/**/*.cs + +# Un-ignore all .gitkeep files anywhere +!**/.gitkeep diff --git a/examples/starter/KedroSpaceflightsSpark/Data/Catalog.cs b/examples/starter/KedroSpaceflightsSpark/Data/Catalog.cs new file mode 100644 index 00000000..ce042df3 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/Catalog.cs @@ -0,0 +1,17 @@ +using Flowthru.Core.Data; +using Flowthru.Extensions.Spark; + +namespace KedroSpaceflightsSpark.Data; + +public partial class Catalog : CatalogAbstract +{ + private readonly string _basePath; + internal readonly SparkFrameProvider frameProvider; + + public Catalog(string basePath, SparkFrameProvider frameProvider) + { + _basePath = basePath; + this.frameProvider = frameProvider; + InitializeCatalogProperties(); + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Catalog.Raw.cs b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Catalog.Raw.cs new file mode 100644 index 00000000..d53c6217 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Catalog.Raw.cs @@ -0,0 +1,35 @@ +using Flowthru.Core.Data; +using KedroSpaceflightsSpark.Data._01_Raw.Schemas; + +namespace KedroSpaceflightsSpark.Data; + +public partial class Catalog +{ + public IItem> Companies => + CreateItem( + () => + ItemFactory.Enumerable.Csv( + label: "Companies", + filePath: $"{_basePath}/_01_Raw/Datasets/companies.csv" + ) + ); + + public IItem> Reviews => + CreateItem( + () => + ItemFactory.Enumerable.Csv( + label: "Reviews", + filePath: $"{_basePath}/_01_Raw/Datasets/reviews.csv" + ) + ); + + public IItem> Shuttles => + CreateItem( + () => + ItemFactory.Enumerable.Excel( + label: "Shuttles", + filePath: $"{_basePath}/_01_Raw/Datasets/shuttles.xlsx", + sheetName: "Sheet1" + ) + ); +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/NOTICE b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/NOTICE new file mode 100644 index 00000000..e292abd8 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/NOTICE @@ -0,0 +1,24 @@ +NOTICE + +The dataset files in this directory (companies.csv, reviews.csv, shuttles.xlsx) +are derived from the Kedro Spaceflights tutorial dataset, which is part of the +kedro-starters project. + +Source: https://github.com/kedro-org/kedro-starters +Copyright: Kedro team (https://github.com/kedro-org) +License: Apache License 2.0 + +These files are included here for demonstration and testing purposes as part of +the Flowthru project's example implementations. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/companies.csv b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/companies.csv new file mode 100644 index 00000000..c3b2ee21 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/companies.csv @@ -0,0 +1,10020 @@ +id,company_rating,company_location,total_fleet_count,iata_approved +3888,100%,Isle of Man,1.0,f +46728,100%,,1.0,f +34618,38%,Isle of Man,1.0,f +28619,100%,Bosnia and Herzegovina,1.0,f +8240,,Chile,1.0,t +16813,100%,Kiribati,2.0,f +2859,90%,Bahrain,1.0,f +33237,,Nicaragua,1.0,f +30052,100%,Turkmenistan,1.0,f +43711,100%,Rwanda,1.0,f +14574,100%,,1.0,f +36935,,Niue,1.0,t +17077,,,1.0,f +28484,33%,Sao Tome and Principe,1.0,t +40075,93%,Denmark,2.0,t +19811,90%,Nicaragua,21.0,t +1337,0%,Micronesia,1.0,f +16846,,Lebanon,1.0,f +22049,,Djibouti,1.0,f +33906,80%,Marshall Islands,1.0,f +14122,100%,Malta,1.0,t +42226,100%,Rwanda,2.0,t +38865,100%,,2.0,t +12594,,,1.0,t +42127,,Chile,1.0,t +12819,,,1.0,f +12322,,Ghana,1.0,t +10973,,Rwanda,1.0,f +23917,,Guinea,1.0,f +32178,,Niue,4.0,t +5332,82%,,5.0,t +9830,100%,Barbados,3.0,t +18007,,Russian Federation,1.0,f +41662,,,1.0,t +43024,,Croatia,1.0,t +23356,100%,Netherlands,1.0,f +16042,0%,Rwanda,2.0,f +18420,,Wallis and Futuna,1.0,t +28637,,Chad,1.0,f +33035,,Gibraltar,1.0,t +37247,100%,Tanzania,3.0,f +47759,,,1.0,f +37293,92%,Russian Federation,15.0,f +9865,,Lebanon,1.0,t +14918,100%,Jersey,1.0,f +38552,100%,Montserrat,1.0,f +27510,,Kenya,1.0,t +25573,,Reunion,1.0,f +3983,,Guinea,1.0,f +41510,100%,Gambia,1.0,f +16449,,Tonga,1.0,t +26524,100%,Guinea,2.0,t +46308,100%,,1.0,f +16370,50%,Isle of Man,1.0,t +15404,,,1.0,t +7588,100%,Philippines,2.0,f +35572,,Nicaragua,1.0,f +29167,,,1.0,t +18953,100%,Chile,1.0,f +21124,,Estonia,2.0,t +4149,,Russian Federation,1.0,f +7891,100%,Barbados,1.0,f +31532,,,1.0,f +22175,50%,Lebanon,1.0,t +24416,,,1.0,t +43870,100%,,1.0,f +15015,,,1.0,t +37686,90%,Monaco,1.0,t +41236,,,1.0,f +22210,,Zimbabwe,1.0,f +40770,100%,Tonga,2.0,f +7399,,,1.0,t +22408,,Portugal,1.0,f +26040,,Isle of Man,1.0,t +3659,,Cape Verde,1.0,t +30020,,,1.0,f +47398,100%,,1.0,t +30561,,Reunion,1.0,f +22090,100%,Marshall Islands,1.0,t +653,,Vanuatu,3.0,f +2904,,Canada,1.0,f +39794,,,1.0,t +30170,,Chile,1.0,t +44781,90%,,1.0,f +24562,,Lithuania,1.0,f +1241,100%,Chile,1.0,f +43897,100%,,1.0,f +4348,,,1.0,f +7008,,,1.0,f +18922,,,1.0,f +34678,89%,Croatia,4.0,t +46270,,Niue,1.0,t +39117,,Moldova,1.0,t +32063,100%,Niue,2.0,t +19581,100%,Russian Federation,1.0,f +42314,100%,,1.0,f +18430,,Lebanon,2.0,t +28985,100%,Monaco,1.0,t +37939,,,1.0,t +32494,,,1.0,t +9703,,,1.0,f +17908,100%,Mexico,2.0,f +2732,100%,Turkmenistan,4.0,f +14496,,Isle of Man,1.0,t +10546,,,1.0,t +19139,100%,Mauritania,1.0,f +42020,100%,,2.0,t +17652,50%,Isle of Man,1.0,f +11023,,,1.0,f +31213,100%,Denmark,1.0,t +46569,,,1.0,f +40661,,Sao Tome and Principe,2.0,t +12483,,,1.0,t +1577,100%,Peru,8.0,t +48446,100%,,1.0,f +15425,,Uzbekistan,1.0,t +9819,,United Kingdom,1.0,t +24954,60%,Christmas Island,2.0,f +24736,100%,Isle of Man,1.0,t +950,,Chile,1.0,f +14812,100%,,1.0,t +46615,100%,,1.0,t +14790,,Niger,1.0,f +16,,,1.0,t +44885,100%,Philippines,1.0,t +12310,,,1.0,f +31040,100%,El Salvador,2.0,t +40396,,Russian Federation,1.0,f +19540,100%,Guinea,1.0,t +20183,100%,,1.0,t +78,100%,Gambia,1.0,t +42549,,Uzbekistan,1.0,t +39998,100%,Micronesia,4.0,t +768,,Bahrain,1.0,t +44637,,El Salvador,1.0,t +44245,100%,,1.0,f +22758,,,1.0,f +47617,100%,Philippines,2.0,f +23779,,,1.0,f +18071,,,1.0,t +8640,,Niue,1.0,t +18651,,,1.0,t +27729,,Afghanistan,4.0,f +37118,,,1.0,t +45083,100%,,1.0,f +7309,100%,Togo,4.0,f +15586,,Niue,1.0,t +33683,33%,Somalia,2.0,f +7056,100%,,2.0,t +12799,80%,Faroe Islands,2.0,f +34355,,,2.0,t +5729,100%,Russian Federation,1.0,f +21239,100%,Denmark,1.0,f +5532,,Jersey,1.0,f +15370,,El Salvador,1.0,f +49953,100%,Niger,1.0,f +31290,,,1.0,f +35774,90%,Brazil,1.0,t +25262,,Maldives,1.0,t +48162,100%,Marshall Islands,1.0,f +5643,,Tonga,1.0,t +17702,,Lebanon,1.0,f +29745,90%,Andorra,1.0,f +25613,,Nicaragua,1.0,t +36263,,,1.0,f +34381,83%,Sao Tome and Principe,1.0,f +1353,,France,1.0,t +22271,,Nicaragua,2.0,f +38299,100%,Gambia,1.0,f +42558,,Russian Federation,1.0,f +8551,100%,Lebanon,1.0,f +42646,33%,Rwanda,4.0,f +4717,,Isle of Man,1.0,f +42975,,Philippines,1.0,f +26853,,Russian Federation,1.0,t +17669,100%,Russian Federation,3.0,f +25029,,,1.0,t +43882,,Turkmenistan,1.0,f +41421,,Gambia,1.0,t +48188,100%,,1.0,f +17751,,,2.0,f +33043,,Niue,2.0,f +20648,,United Kingdom,1.0,f +22110,100%,,1.0,t +830,,Guernsey,1.0,t +10364,100%,Slovakia (Slovak Republic),14.0,f +27226,100%,Congo,10.0,f +55,100%,Kiribati,2.0,f +44796,100%,Maldives,2.0,t +7025,100%,Jersey,1.0,f +19731,,,1.0,t +30130,100%,Togo,1.0,f +26794,100%,Lithuania,1.0,f +17051,100%,Micronesia,1.0,f +8012,100%,Sao Tome and Principe,2.0,t +15991,100%,Congo,1.0,t +42600,100%,Malawi,1.0,t +6669,,Malawi,1.0,t +46787,100%,Niue,1.0,t +11291,100%,,1.0,f +35323,89%,,1.0,f +2721,100%,,1.0,t +19554,,Lebanon,2.0,f +22806,100%,Faroe Islands,1.0,f +33223,100%,Nauru,1.0,t +547,,Guinea,1.0,t +36422,,Montserrat,1.0,f +23387,100%,,2.0,t +20463,100%,Lithuania,2.0,f +6860,,Djibouti,1.0,f +28276,83%,Papua New Guinea,2.0,f +26519,100%,Turks and Caicos Islands,2.0,t +6930,,Guernsey,1.0,t +31598,67%,Ukraine,1.0,f +18366,78%,,1.0,f +35826,,,2.0,f +12261,,Niue,1.0,t +47159,88%,Denmark,1.0,t +42671,,Niue,1.0,f +33222,67%,Croatia,1.0,f +6984,100%,Suriname,9.0,f +8362,100%,Malawi,2.0,t +28914,,Chile,1.0,t +17,,,1.0,f +28030,90%,Anguilla,1.0,f +46381,100%,,2.0,f +11904,100%,Nauru,1.0,f +7842,100%,,1.0,f +46676,,Lebanon,1.0,t +30684,100%,Niue,2.0,t +36627,,,2.0,t +40047,,Malta,1.0,f +44736,100%,Guernsey,1.0,t +21217,75%,Nauru,1.0,f +10240,,,1.0,f +48629,,Micronesia,1.0,f +39070,100%,,1.0,f +35428,,Switzerland,1.0,t +46424,,Niger,6.0,t +46591,,French Guiana,1.0,f +14136,100%,Guinea,1.0,f +49604,,Afghanistan,2.0,t +44127,100%,Rwanda,1.0,t +30462,0%,Lithuania,1.0,t +35077,100%,Cocos (Keeling) Islands,4.0,f +37835,100%,,1.0,t +28404,100%,Croatia,1.0,t +7612,100%,,1.0,f +35386,100%,Venezuela,1.0,f +44417,100%,,1.0,f +7217,,Sao Tome and Principe,1.0,f +26952,,Russian Federation,3.0,t +18182,67%,,1.0,f +5450,,,1.0,f +31437,90%,Denmark,1.0,f +24676,,Guinea,1.0,f +49510,100%,Croatia,1.0,t +26064,,,1.0,f +39923,100%,Russian Federation,2.0,f +46440,,Russian Federation,1.0,t +37178,,,1.0,f +3545,,Pakistan,1.0,t +33059,,Malta,1.0,f +27823,100%,,1.0,t +34829,100%,,2.0,t +29643,,,1.0,t +46932,,,1.0,f +41024,95%,Uzbekistan,2.0,f +18557,67%,Monaco,2.0,f +16464,,Isle of Man,1.0,f +45171,100%,Kiribati,1.0,t +10426,,,1.0,f +45766,,,2.0,t +34860,50%,,1.0,f +36423,100%,,1.0,f +14872,,China,1.0,t +48212,,,1.0,f +45880,,,1.0,t +24060,100%,Malta,1.0,t +40276,100%,Anguilla,1.0,t +16649,100%,Gibraltar,8.0,f +8956,100%,Kiribati,1.0,f +38957,,Kiribati,1.0,f +33622,,Micronesia,1.0,f +9411,,Papua New Guinea,1.0,t +38297,100%,,3.0,f +21292,,Gambia,1.0,t +7656,100%,Russian Federation,1.0,f +1923,,,1.0,f +4486,100%,Russian Federation,1.0,t +9420,,,1.0,f +10593,,Netherlands,1.0,f +33188,,Indonesia,1.0,f +14471,,Micronesia,1.0,f +42307,,,1.0,f +41099,,,1.0,f +42163,,Rwanda,1.0,t +18666,,Cocos (Keeling) Islands,2.0,t +10340,,Ecuador,1.0,t +35534,,Ghana,1.0,f +7224,,Brunei Darussalam,1.0,t +31184,100%,,1.0,t +24583,,Bahrain,1.0,f +6786,,Niue,1.0,t +10887,67%,Russian Federation,1.0,f +29614,100%,San Marino,1.0,f +33012,,,1.0,f +36581,,,1.0,f +24475,,,1.0,f +9318,,,1.0,f +38257,100%,Svalbard & Jan Mayen Islands,1.0,f +42777,,,1.0,f +17016,,,1.0,t +24405,90%,,1.0,f +26957,100%,Uzbekistan,1.0,f +21860,100%,France,2.0,t +42892,,Tanzania,1.0,f +11298,,Zimbabwe,1.0,f +21816,,Russian Federation,1.0,f +16292,83%,Reunion,1.0,f +25399,,,1.0,t +39857,100%,Cocos (Keeling) Islands,1.0,t +17150,,Vanuatu,2.0,t +11618,,Montserrat,2.0,f +33743,100%,Denmark,2.0,f +4957,100%,Puerto Rico,1.0,f +34238,100%,Saint Barthelemy,5.0,t +45429,100%,Niue,1.0,f +9437,,,1.0,f +41584,,,1.0,t +12213,,Chad,1.0,t +14348,100%,,1.0,f +37243,100%,Tunisia,1.0,f +18338,88%,Ecuador,1.0,t +21186,100%,Rwanda,2.0,t +14575,,,1.0,f +5108,,Turkmenistan,1.0,f +13427,100%,Ecuador,1.0,f +42080,100%,France,1.0,f +12085,,,1.0,f +40805,90%,Uzbekistan,2.0,f +46907,,Chile,1.0,t +28255,75%,Somalia,1.0,f +1677,,,1.0,f +8071,,,1.0,f +8972,100%,,1.0,f +14256,100%,,3.0,f +47264,,,1.0,t +27948,100%,Niue,4.0,f +32413,100%,Faroe Islands,1.0,f +5848,80%,Canada,2.0,f +13207,100%,,1.0,f +5484,,Papua New Guinea,1.0,f +29938,,Turkmenistan,1.0,t +44666,100%,Uzbekistan,1.0,t +19877,,Zimbabwe,1.0,f +36756,,Indonesia,2.0,t +18919,,Russian Federation,1.0,t +46077,100%,,1.0,f +37326,100%,,1.0,f +9566,,Isle of Man,1.0,t +43992,100%,Uzbekistan,3.0,t +31667,100%,Croatia,1.0,f +15866,100%,Papua New Guinea,2.0,f +32784,,Cocos (Keeling) Islands,1.0,t +28172,100%,Isle of Man,1.0,f +33330,100%,,1.0,t +33895,100%,,1.0,f +28745,,,1.0,f +21649,100%,,1.0,t +14050,100%,Lithuania,1.0,t +33950,100%,Gambia,1.0,t +38508,100%,Rwanda,1.0,t +13074,100%,Turks and Caicos Islands,1.0,t +49786,,Tonga,1.0,f +9857,100%,,1.0,t +21016,,,1.0,f +37331,,Bosnia and Herzegovina,1.0,f +1616,100%,Zimbabwe,1.0,t +42104,50%,Djibouti,2.0,f +18212,,,1.0,f +43001,,Bosnia and Herzegovina,1.0,f +7043,,Malta,1.0,t +20744,,Niger,2.0,f +8897,,,1.0,t +19241,100%,Afghanistan,1.0,t +6806,,,1.0,f +18928,90%,,1.0,f +45524,,,1.0,f +15985,67%,Somalia,1.0,f +32650,100%,,2.0,f +3635,,,1.0,f +39919,90%,Guinea,3.0,t +29950,,France,1.0,t +13550,,Micronesia,1.0,f +29070,100%,Micronesia,1.0,f +38004,0%,Palestinian Territory,2.0,f +29844,100%,,2.0,t +29866,100%,Isle of Man,2.0,f +10233,,Barbados,2.0,f +8878,,Nauru,2.0,f +22419,,United Kingdom,1.0,f +44613,100%,,1.0,t +36766,100%,Malta,1.0,f +23055,91%,,2.0,f +614,67%,,1.0,f +6674,,Tonga,2.0,t +48698,90%,Vietnam,3.0,f +8908,100%,,2.0,f +21008,100%,Bouvet Island (Bouvetoya),1.0,f +6035,,Afghanistan,1.0,f +39878,0%,,1.0,f +517,100%,United Kingdom,1.0,f +48181,,,1.0,f +4269,,,1.0,f +25431,100%,Sao Tome and Principe,1.0,t +45199,,Puerto Rico,6.0,f +27067,,,1.0,f +20314,100%,El Salvador,1.0,f +30790,,Reunion,2.0,f +47479,100%,Russian Federation,1.0,t +11711,,Korea,1.0,t +29382,100%,Ghana,4.0,f +4964,100%,Malawi,1.0,t +18698,100%,,1.0,f +25308,100%,Guinea,1.0,t +15475,100%,Chad,1.0,f +15068,100%,Sao Tome and Principe,1.0,f +20663,100%,,1.0,t +42762,100%,United Kingdom,1.0,f +48184,,Tanzania,1.0,t +29328,,Moldova,1.0,t +14921,100%,Micronesia,2.0,f +5496,,Maldives,1.0,t +33948,100%,,2.0,f +31185,100%,,1.0,f +1328,,Kenya,2.0,f +31600,,Papua New Guinea,1.0,f +590,,Guinea,2.0,t +39575,,,1.0,f +25445,70%,,2.0,f +11781,100%,Guinea,2.0,t +38634,100%,Malta,1.0,t +42513,,Niger,1.0,f +13547,,,1.0,t +10079,100%,Rwanda,1.0,f +28224,100%,Rwanda,1.0,f +41372,,Indonesia,2.0,t +36681,100%,Tunisia,1.0,t +28131,100%,,1.0,t +46996,100%,Tonga,1.0,f +1247,,Chile,1.0,t +41022,100%,Cape Verde,3.0,t +40823,100%,,1.0,f +32703,100%,,2.0,f +17447,100%,El Salvador,4.0,t +27442,90%,Solomon Islands,1.0,f +12701,100%,Monaco,1.0,f +13786,,,1.0,f +16550,100%,Isle of Man,1.0,f +43252,100%,Uzbekistan,1.0,t +19861,67%,Togo,1.0,f +7058,100%,Isle of Man,2.0,f +25438,100%,,1.0,f +46687,100%,,1.0,f +36288,100%,Marshall Islands,1.0,f +19278,,,1.0,f +35824,,Anguilla,1.0,f +40051,,,1.0,f +36914,100%,Nauru,1.0,t +35737,100%,,1.0,f +23857,100%,Isle of Man,1.0,f +20619,,,1.0,t +16573,100%,Cuba,2.0,f +6522,,Lithuania,1.0,f +32801,63%,Somalia,1.0,f +47812,100%,Maldives,1.0,f +17138,100%,Isle of Man,1.0,t +11110,88%,Kiribati,3.0,t +3314,100%,Slovakia (Slovak Republic),10.0,f +2394,,,2.0,t +17061,100%,Niue,2.0,f +21275,100%,Marshall Islands,1.0,f +22745,,Indonesia,1.0,f +24243,,,2.0,t +24380,,Zimbabwe,2.0,f +4898,100%,Kiribati,1.0,f +18326,100%,Monaco,1.0,f +39510,,Guinea,1.0,f +19681,,,2.0,t +12910,,Maldives,1.0,t +43932,100%,,1.0,f +26159,100%,Turkmenistan,1.0,t +7620,100%,Togo,4.0,f +14319,,Afghanistan,1.0,f +44146,36%,Malawi,2.0,t +16916,,Guernsey,1.0,f +45842,100%,Maldives,1.0,t +12465,,Marshall Islands,1.0,f +6198,20%,,1.0,f +16581,,Guinea,1.0,t +16697,,Sao Tome and Principe,1.0,f +22132,,Kenya,1.0,f +46763,,China,1.0,t +44280,,Iraq,2.0,t +23440,,Chad,1.0,t +15505,100%,,1.0,f +26079,100%,Zimbabwe,1.0,f +3279,,Uzbekistan,1.0,t +22112,90%,,1.0,f +6759,,,1.0,t +9570,100%,Micronesia,1.0,f +26292,100%,Jersey,2.0,t +41270,100%,,1.0,f +25183,,Malta,1.0,t +4953,,Saint Helena,1.0,f +27090,100%,Tonga,2.0,t +16087,,French Guiana,1.0,f +43902,100%,,2.0,t +18490,,,1.0,f +16519,,Cocos (Keeling) Islands,1.0,f +45210,,Zimbabwe,1.0,t +44687,,Russian Federation,1.0,f +47302,,Netherlands,2.0,f +1200,100%,Russian Federation,2.0,t +525,100%,,1.0,f +42454,100%,Finland,1.0,f +32276,100%,Kiribati,1.0,f +27227,100%,Isle of Man,2.0,t +7237,100%,Guinea,1.0,t +13082,100%,Kiribati,3.0,t +27007,0%,,1.0,t +37454,100%,,2.0,f +42913,,Lebanon,1.0,t +9586,100%,Guinea,2.0,t +2480,100%,,1.0,f +23953,100%,,1.0,f +3490,100%,Papua New Guinea,1.0,f +7321,,Papua New Guinea,1.0,f +43891,,China,2.0,t +32587,,Jersey,1.0,f +29480,100%,French Polynesia,6.0,f +26901,100%,,1.0,t +32658,85%,Saint Helena,1.0,f +37841,0%,,1.0,t +5067,,Tonga,1.0,f +38160,25%,Nicaragua,1.0,f +25013,,Tonga,1.0,t +12899,50%,,1.0,t +9803,,Monaco,1.0,t +4094,,Russian Federation,4.0,t +1327,100%,Brazil,1.0,f +8755,,Malawi,1.0,f +33025,,Tonga,1.0,f +27289,99%,Reunion,12.0,t +23224,100%,,1.0,f +33783,0%,Lebanon,1.0,f +45197,,Christmas Island,1.0,f +35226,50%,,1.0,f +37283,100%,Brazil,1.0,t +18509,,Gambia,1.0,f +11005,100%,,1.0,t +38193,100%,French Guiana,1.0,t +8850,,,1.0,t +1133,,,1.0,t +49484,,Russian Federation,1.0,f +21331,100%,Holy See (Vatican City State),1.0,f +45597,,,1.0,t +40192,100%,Croatia,2.0,t +32691,100%,Nicaragua,1.0,f +10697,100%,Uzbekistan,3.0,t +35364,100%,Sao Tome and Principe,1.0,f +26863,,Mexico,7.0,f +37715,,Guinea,1.0,t +44354,100%,Russian Federation,1.0,t +18966,100%,,2.0,f +45281,,,1.0,f +1126,100%,Estonia,1.0,t +24205,100%,,1.0,f +34121,,,1.0,f +27811,100%,Ukraine,4.0,t +27966,100%,,1.0,f +24646,100%,Barbados,1.0,f +2078,,Sao Tome and Principe,1.0,f +44257,,Lebanon,1.0,t +11986,,,1.0,f +14985,,,2.0,t +24254,,,1.0,f +16258,100%,,1.0,t +45680,,Russian Federation,1.0,t +45208,100%,Svalbard & Jan Mayen Islands,1.0,f +16306,,Holy See (Vatican City State),1.0,f +7246,100%,Niue,6.0,f +41675,100%,Zimbabwe,1.0,f +24351,,Marshall Islands,1.0,t +29316,,Faroe Islands,1.0,t +1373,,Brazil,1.0,f +31515,,,1.0,f +29343,,Russian Federation,1.0,f +36832,83%,Micronesia,1.0,f +13474,80%,Zimbabwe,7.0,t +21341,80%,Tonga,1.0,f +4308,100%,Togo,1.0,f +45369,,Christmas Island,1.0,f +18024,100%,Monaco,1.0,f +4630,100%,,2.0,f +23911,,Vanuatu,1.0,t +46453,100%,Russian Federation,1.0,t +41036,100%,French Polynesia,2.0,f +5405,,,1.0,t +1298,100%,,1.0,t +14741,,,1.0,t +15803,100%,Tonga,1.0,t +43369,,Faroe Islands,1.0,t +24434,,Marshall Islands,1.0,t +44254,100%,,2.0,t +841,,Mauritania,1.0,f +29525,,,1.0,f +42156,,French Guiana,1.0,t +10355,,,1.0,f +24610,90%,,1.0,f +3189,100%,Marshall Islands,2.0,t +47361,100%,Barbados,10.0,f +13272,,Croatia,1.0,t +30971,,,1.0,f +33930,,El Salvador,1.0,t +35563,,French Guiana,1.0,f +17568,100%,,2.0,f +19813,,Jersey,1.0,f +18791,100%,Zimbabwe,8.0,t +17427,100%,Mexico,2.0,f +16173,100%,Rwanda,1.0,f +23610,,Uzbekistan,1.0,t +37595,90%,Lithuania,4.0,f +13557,80%,,1.0,t +4010,100%,,1.0,t +43393,100%,,1.0,f +14052,,Sao Tome and Principe,1.0,f +6760,,Montserrat,1.0,f +40143,,Russian Federation,1.0,t +40862,,Guinea,1.0,f +2180,0%,Australia,1.0,f +41507,100%,Sao Tome and Principe,1.0,t +4854,100%,Monaco,1.0,f +15306,,Barbados,8.0,t +8524,,Faroe Islands,1.0,f +21736,100%,Vanuatu,4.0,f +19955,,Rwanda,1.0,t +33190,0%,Anguilla,1.0,t +34241,,Gibraltar,1.0,t +41873,100%,Zimbabwe,2.0,f +29216,,,1.0,f +7500,100%,Paraguay,8.0,t +515,100%,Mauritania,1.0,f +41066,,,1.0,f +38533,,Chile,1.0,t +16507,100%,Nauru,1.0,f +13827,,Turks and Caicos Islands,1.0,f +25415,100%,Ghana,1.0,f +20760,,French Guiana,1.0,f +16655,80%,,1.0,f +5941,100%,Gambia,2.0,f +24582,,Netherlands,2.0,t +23344,100%,Grenada,1.0,f +16868,100%,,1.0,t +19653,,Cook Islands,1.0,f +30866,,,1.0,f +16441,,Ghana,1.0,t +19806,60%,Papua New Guinea,1.0,f +41296,100%,Svalbard & Jan Mayen Islands,1.0,f +44421,,Monaco,1.0,f +40264,100%,Russian Federation,1.0,f +17712,100%,,1.0,f +832,,El Salvador,1.0,f +28236,,Greenland,1.0,f +36429,,,1.0,f +7244,100%,Guatemala,2.0,t +44834,97%,Guernsey,4.0,f +32401,,,1.0,f +17086,100%,Saint Pierre and Miquelon,2.0,t +36900,100%,,1.0,t +44820,,,1.0,f +21557,,,1.0,f +29526,,Sao Tome and Principe,1.0,t +22096,,,1.0,f +37066,100%,Micronesia,1.0,t +37117,100%,Slovakia (Slovak Republic),2.0,t +17819,100%,Guinea,2.0,f +9202,100%,,1.0,f +39561,100%,,2.0,t +2924,,Puerto Rico,1.0,f +23914,,Micronesia,1.0,f +38377,,Lebanon,1.0,f +5345,100%,,2.0,f +23170,100%,Sao Tome and Principe,2.0,t +36924,,Sao Tome and Principe,1.0,t +3947,,Zimbabwe,1.0,f +42996,,Jersey,1.0,f +28920,,Malta,1.0,t +11126,100%,Anguilla,3.0,t +20283,100%,Denmark,1.0,f +24271,,Kenya,1.0,f +47546,0%,,1.0,f +38524,100%,Libyan Arab Jamahiriya,1.0,t +26233,,,2.0,t +30740,,Gambia,2.0,t +10475,,Mauritania,1.0,t +49825,100%,Pakistan,7.0,t +47391,,,2.0,t +39894,,,1.0,f +39637,100%,Niue,1.0,f +3290,,Solomon Islands,1.0,f +29972,,United Kingdom,1.0,f +6993,100%,Costa Rica,1.0,t +37356,,Guinea,3.0,f +2941,,Tonga,2.0,t +29798,,,1.0,f +48898,,Niue,1.0,f +29323,100%,Guinea,3.0,f +39609,,United Kingdom,1.0,f +8525,,Sao Tome and Principe,2.0,f +10636,100%,Jersey,1.0,f +12911,100%,Maldives,1.0,f +22231,100%,Guinea,2.0,f +3302,,,1.0,f +44507,83%,,1.0,f +7437,,Niue,1.0,t +33669,100%,Malta,2.0,t +31773,100%,,1.0,t +27738,,Chad,1.0,t +30644,,Kiribati,1.0,t +19309,100%,Maldives,3.0,t +4868,100%,Jersey,2.0,t +44440,88%,Somalia,2.0,f +4780,100%,Svalbard & Jan Mayen Islands,1.0,f +22364,100%,Denmark,2.0,f +22799,,,1.0,t +24546,50%,Zimbabwe,2.0,f +45111,100%,Sao Tome and Principe,420.0,f +814,,,1.0,f +40899,100%,,1.0,t +49650,,Isle of Man,1.0,t +38122,100%,Tonga,1.0,f +28667,100%,Barbados,1.0,f +16906,,Costa Rica,1.0,t +34467,100%,Indonesia,1.0,f +17499,33%,Somalia,1.0,t +31352,100%,Pakistan,1.0,f +390,100%,Niue,1.0,t +49805,,,1.0,f +41646,,Maldives,3.0,f +12187,,,1.0,f +39441,96%,Cuba,4.0,f +10149,100%,Isle of Man,1.0,f +44861,,Puerto Rico,1.0,f +8355,87%,Togo,16.0,f +20114,100%,Croatia,1.0,t +7303,,,1.0,f +6780,,Zimbabwe,2.0,t +41999,,Mexico,1.0,f +8050,,Nicaragua,1.0,f +40479,100%,France,1.0,f +8831,,,3.0,f +41751,100%,,1.0,f +5346,100%,,1.0,f +45569,,,1.0,t +28743,100%,Somalia,1.0,t +31554,100%,Marshall Islands,1.0,t +12512,,Greenland,1.0,f +20945,,Niger,1.0,f +14825,,Uzbekistan,1.0,f +31839,100%,Mauritania,1.0,f +4828,100%,Philippines,1.0,t +5483,,Guinea,1.0,f +32524,100%,Nicaragua,1.0,t +28803,100%,Cocos (Keeling) Islands,1.0,f +38357,0%,,1.0,t +11329,,Philippines,1.0,f +27822,100%,Russian Federation,2.0,f +41967,,,1.0,f +29454,100%,Guinea,1.0,f +43770,,Rwanda,1.0,t +15478,80%,,1.0,f +27460,50%,Denmark,2.0,f +6053,,Sao Tome and Principe,1.0,t +15200,100%,Gibraltar,1.0,t +15452,90%,Niue,2.0,f +29244,67%,Micronesia,1.0,f +41477,,,1.0,t +4659,,,1.0,t +20335,100%,,2.0,f +42498,,Malawi,1.0,t +2643,,Anguilla,1.0,t +43734,90%,,1.0,f +16438,,,1.0,t +21752,90%,Turks and Caicos Islands,1.0,f +5970,100%,El Salvador,2.0,f +9297,,Tanzania,1.0,t +4283,,Nicaragua,1.0,t +31372,100%,Russian Federation,1.0,t +9715,100%,Russian Federation,1.0,t +23252,,,1.0,f +2538,67%,Monaco,1.0,f +32849,,,1.0,t +38912,,Ghana,1.0,f +27158,100%,Indonesia,1.0,f +14735,,Greenland,1.0,f +31055,64%,Niue,2.0,f +9270,100%,Micronesia,1.0,f +5703,,,1.0,f +6004,90%,Micronesia,4.0,f +40951,100%,Turkmenistan,1.0,f +44686,,Sao Tome and Principe,1.0,t +21580,100%,,3.0,f +22471,,Estonia,1.0,t +21669,,Bosnia and Herzegovina,1.0,f +36760,,Isle of Man,1.0,t +22770,,Puerto Rico,1.0,f +10317,,Venezuela,1.0,f +27092,,Brazil,2.0,f +30160,,Niue,1.0,f +24440,100%,Uruguay,1.0,f +36019,100%,Niue,1.0,f +1840,,,1.0,f +9578,,Mexico,1.0,f +36196,90%,Gibraltar,1.0,f +1167,,Guinea,1.0,t +35797,,,1.0,f +5031,,El Salvador,1.0,f +30320,100%,Togo,4.0,f +41661,,Vietnam,1.0,t +48321,100%,China,17.0,f +25766,75%,Chile,1.0,t +41553,100%,Tonga,2.0,f +14325,,Maldives,2.0,t +170,,,1.0,f +20316,,Isle of Man,1.0,t +21045,100%,French Guiana,2.0,f +43903,100%,,1.0,f +1330,100%,United Kingdom,1.0,t +8294,0%,Zimbabwe,1.0,f +40484,100%,,1.0,t +33635,,Reunion,1.0,f +32075,100%,Indonesia,2.0,f +46099,98%,Uzbekistan,28.0,f +15112,,Malawi,2.0,f +44978,,Philippines,1.0,f +38733,100%,,2.0,f +22030,100%,French Guiana,3.0,t +16069,100%,Micronesia,1.0,f +44152,,Malta,1.0,f +12963,,,1.0,f +49105,100%,Malta,3.0,f +46010,100%,,1.0,f +14395,100%,Sao Tome and Principe,1.0,t +326,100%,Gambia,1.0,t +6986,,Micronesia,1.0,t +42930,,French Guiana,1.0,f +17316,100%,Niue,1.0,t +10723,100%,,1.0,f +39793,100%,Uzbekistan,1.0,t +21431,100%,,2.0,t +18963,,Anguilla,1.0,f +28789,100%,,3.0,t +30927,100%,Indonesia,1.0,f +3620,100%,Russian Federation,1.0,f +39301,100%,,1.0,f +43146,100%,,1.0,f +29574,,,1.0,f +37529,,Tonga,1.0,f +35028,,Chad,1.0,f +33342,,Nicaragua,1.0,f +41738,,Jersey,1.0,f +22460,,China,1.0,t +27232,,Faroe Islands,1.0,t +27766,75%,,1.0,t +4294,100%,,1.0,t +38032,,Svalbard & Jan Mayen Islands,1.0,t +25475,,,1.0,f +27106,100%,China,1.0,f +8653,,Tonga,1.0,t +35312,100%,,1.0,f +36659,100%,Anguilla,2.0,t +35190,100%,,1.0,t +23371,,,1.0,f +14700,100%,Montserrat,3.0,f +25367,,Sao Tome and Principe,1.0,t +16274,,Niger,1.0,t +27363,80%,,3.0,t +44851,,Lebanon,1.0,f +7264,100%,Philippines,9.0,f +44895,100%,France,3.0,t +12033,,Congo,1.0,f +29678,100%,Nicaragua,1.0,f +19915,100%,,1.0,f +49681,,France,2.0,f +48519,,Isle of Man,1.0,t +34272,,Papua New Guinea,1.0,t +8338,75%,Korea,2.0,f +24567,90%,Costa Rica,2.0,f +21201,,Vanuatu,2.0,f +16737,100%,Kiribati,1.0,t +43429,,Turks and Caicos Islands,2.0,f +37549,100%,Chile,1.0,t +34685,,Chad,1.0,t +10913,,,2.0,t +35687,,Djibouti,1.0,t +14083,100%,,2.0,t +40246,,Gibraltar,1.0,f +32616,90%,,3.0,f +2203,100%,Ecuador,1.0,f +14406,100%,,1.0,f +3927,100%,Niue,1.0,t +46254,100%,Venezuela,4.0,f +44788,92%,,1.0,f +20664,100%,Nauru,4.0,f +37947,100%,Jersey,1.0,f +32090,100%,,1.0,t +43733,,Chile,1.0,f +32697,,Lithuania,1.0,t +35947,,,1.0,f +46973,100%,Malta,2.0,t +20150,100%,Jersey,1.0,f +21623,100%,Maldives,1.0,f +25180,100%,,1.0,t +49998,100%,Croatia,1.0,t +7927,100%,Chad,1.0,f +16040,100%,,1.0,f +33973,,,1.0,f +11653,,Malta,1.0,f +23304,,Barbados,1.0,t +44627,100%,Cocos (Keeling) Islands,1.0,f +9430,,Sao Tome and Principe,1.0,t +25659,100%,United Kingdom,1.0,t +28568,100%,Faroe Islands,3.0,f +7988,100%,Mauritania,1.0,f +46806,100%,Maldives,1.0,f +9245,,El Salvador,2.0,t +17627,100%,Guinea,2.0,f +42081,,Mauritania,1.0,t +39233,,,1.0,t +9288,,,1.0,f +38734,100%,,1.0,t +35106,,Togo,1.0,f +44246,,Mauritania,1.0,f +31281,100%,Nauru,1.0,f +12801,,,1.0,t +47051,100%,Zimbabwe,1.0,t +37249,,Malta,1.0,f +21571,,Finland,1.0,t +22522,100%,France,3.0,t +37917,100%,Nauru,1.0,f +46953,100%,Niger,1.0,f +24252,80%,Tonga,2.0,f +11891,100%,Russian Federation,1.0,t +35762,86%,Russian Federation,1.0,f +48029,100%,Malta,2.0,f +28999,,,1.0,f +26043,,Niue,1.0,t +38536,,Togo,1.0,f +27875,100%,Guinea,1.0,f +5943,100%,Russian Federation,1.0,f +48342,100%,Russian Federation,1.0,f +29551,100%,Mauritania,2.0,f +44802,67%,Bouvet Island (Bouvetoya),1.0,f +16165,,France,3.0,f +33882,100%,,1.0,f +3650,100%,Zimbabwe,1.0,t +33791,100%,Guinea,2.0,t +42012,100%,Monaco,3.0,t +43706,,,1.0,f +38574,83%,,1.0,f +17380,50%,Jersey,1.0,t +3680,100%,Indonesia,1.0,f +42686,,,1.0,t +6299,100%,,1.0,t +7053,100%,Marshall Islands,2.0,f +40709,,Denmark,1.0,f +9304,,,, +46357,100%,,1.0,f +1132,88%,Tonga,1.0,f +43276,100%,,2.0,t +29162,,Rwanda,2.0,f +24103,100%,United Kingdom,3.0,f +35638,,Anguilla,1.0,t +19979,100%,Tanzania,1.0,f +13087,100%,Gibraltar,1.0,t +34931,100%,Gibraltar,2.0,t +43111,,Tonga,1.0,f +4763,,Uruguay,2.0,f +31946,,Gambia,1.0,f +10253,,,1.0,f +40314,100%,Gambia,1.0,f +29943,,Rwanda,1.0,t +47734,100%,Ecuador,1.0,t +45289,0%,Marshall Islands,1.0,f +47110,,Nauru,1.0,t +39588,,Micronesia,1.0,t +8880,0%,Kiribati,1.0,f +32532,,Niue,1.0,t +13678,,,2.0,f +1513,,,3.0,f +35809,,,1.0,f +28633,60%,Russian Federation,1.0,t +45579,100%,Togo,1.0,f +3527,,Turks and Caicos Islands,1.0,f +14305,100%,,1.0,t +44077,57%,Djibouti,1.0,f +28398,100%,El Salvador,7.0,t +38207,100%,Turks and Caicos Islands,2.0,t +21498,90%,,1.0,f +24481,,,1.0,f +38323,100%,,1.0,f +5567,100%,Bosnia and Herzegovina,2.0,t +49512,,Netherlands,1.0,t +2106,100%,Uzbekistan,1.0,f +10763,100%,,1.0,t +1820,100%,El Salvador,2.0,f +36119,62%,Isle of Man,1.0,t +38415,,Uzbekistan,1.0,t +27386,,Chad,2.0,f +9028,,Russian Federation,1.0,f +24678,,Faroe Islands,2.0,f +31497,,,1.0,f +15618,,Mauritania,1.0,f +33721,100%,,1.0,t +7498,,Zimbabwe,2.0,t +30679,100%,,1.0,f +35793,,Marshall Islands,1.0,t +26860,,,1.0,f +19303,100%,,1.0,f +34749,,Senegal,1.0,f +9693,100%,Vanuatu,8.0,f +43955,,Tonga,1.0,t +4069,100%,Togo,1.0,f +24337,,Sao Tome and Principe,1.0,f +23404,91%,Malta,1.0,f +29475,50%,Niue,1.0,t +28218,,,1.0,t +21353,,Monaco,1.0,t +22233,100%,,3.0,f +35444,100%,Anguilla,1.0,f +13520,100%,,1.0,f +2620,,Pakistan,1.0,t +26035,100%,Marshall Islands,1.0,f +42567,100%,Palestinian Territory,1.0,f +13200,,China,2.0,f +28719,100%,Russian Federation,4.0,t +6290,,Barbados,1.0,f +46884,100%,Turks and Caicos Islands,2.0,f +12907,100%,Marshall Islands,1.0,f +32669,,France,1.0,f +9759,100%,Isle of Man,1.0,t +40371,100%,Sao Tome and Principe,1.0,f +634,,,1.0,f +18277,,Bosnia and Herzegovina,1.0,f +29339,,Tunisia,1.0,f +22335,100%,Russian Federation,3.0,t +36923,,Kenya,1.0,f +48579,,,1.0,f +32294,,Niue,1.0,f +10294,100%,Puerto Rico,1.0,f +42421,100%,Costa Rica,5.0,t +26548,,,1.0,t +3160,90%,Russian Federation,2.0,f +3452,,Isle of Man,1.0,t +41485,100%,,1.0,t +40858,100%,France,1.0,f +50024,,Micronesia,1.0,f +15766,,,1.0,f +18289,,Guinea,1.0,f +19317,,Russian Federation,1.0,f +46830,100%,,2.0,f +49128,,,1.0,f +40415,,Monaco,1.0,f +46069,50%,France,1.0,t +9743,100%,,2.0,f +15051,100%,,1.0,f +32271,100%,,1.0,f +50028,,Lebanon,1.0,t +23489,,,1.0,t +33338,,Gibraltar,1.0,t +1526,100%,France,1.0,t +17597,33%,Wallis and Futuna,1.0,t +2490,,Niue,1.0,t +25729,100%,Cuba,2.0,f +5797,,Gambia,2.0,f +2258,100%,,3.0,t +9075,,,1.0,f +48175,50%,,1.0,t +14857,,Jersey,1.0,f +45765,100%,,1.0,f +72,,Sao Tome and Principe,1.0,f +41033,,Zimbabwe,1.0,f +3241,90%,Sao Tome and Principe,1.0,t +43430,100%,Kenya,1.0,f +4329,,Niue,1.0,f +17362,,Chad,1.0,f +21485,,Cape Verde,1.0,t +46340,100%,Denmark,3.0,t +44228,,Jersey,1.0,f +43293,,Denmark,2.0,f +1372,,Estonia,1.0,f +3061,,Libyan Arab Jamahiriya,2.0,t +39782,,Denmark,1.0,t +8579,100%,Lithuania,2.0,f +143,,Bouvet Island (Bouvetoya),1.0,t +15723,100%,Gambia,1.0,f +28704,,Cocos (Keeling) Islands,1.0,t +24534,,Barbados,1.0,f +4946,,Chad,1.0,t +1765,,Rwanda,1.0,t +13163,25%,Svalbard & Jan Mayen Islands,2.0,f +30549,100%,Mauritania,1.0,t +25619,100%,,1.0,f +24964,,,1.0,t +23013,90%,Isle of Man,1.0,f +27095,,Nicaragua,2.0,f +27547,,,1.0,t +34481,100%,,2.0,f +32398,100%,Turkmenistan,1.0,f +40650,,,8.0,t +21618,,,1.0,f +15566,,,1.0,f +3695,,,1.0,f +35623,,Micronesia,1.0,f +16232,,Lebanon,1.0,t +18126,,Guinea,1.0,f +32399,100%,Somalia,1.0,t +15756,94%,Afghanistan,2.0,t +42291,,Lebanon,1.0,f +38484,100%,Senegal,2.0,f +28384,,,1.0,t +26368,100%,Brazil,2.0,f +18123,100%,Russian Federation,3.0,f +22646,100%,Anguilla,1.0,t +44948,100%,Chad,3.0,f +8650,100%,,1.0,t +47483,90%,Jersey,1.0,t +22750,,Barbados,4.0,t +9025,88%,Micronesia,1.0,t +19831,100%,Reunion,2.0,f +20550,,Tanzania,1.0,f +46290,100%,Micronesia,2.0,f +31255,,,1.0,t +2318,70%,,1.0,t +3596,100%,Marshall Islands,6.0,t +47004,,Venezuela,1.0,f +5355,,,1.0,f +1021,100%,Jersey,1.0,f +502,,,2.0,f +14583,,Costa Rica,1.0,f +36871,,Chile,1.0,t +1254,100%,,1.0,t +10533,100%,Indonesia,1.0,t +41986,100%,Congo,1.0,t +44635,,Barbados,2.0,t +37506,,Uzbekistan,1.0,f +33860,100%,Niger,1.0,f +12086,,Chad,1.0,t +39358,100%,Kiribati,2.0,f +44916,100%,Congo,2.0,f +15236,100%,Kenya,1.0,t +12433,50%,Lebanon,1.0,t +15375,,,1.0,f +1345,67%,Philippines,1.0,f +41458,,Wallis and Futuna,1.0,t +10856,40%,,1.0,f +35009,,,1.0,t +17757,100%,Uganda,1.0,f +9524,,,1.0,f +46346,,Niue,1.0,t +31377,100%,Isle of Man,1.0,f +12960,100%,Indonesia,9.0,f +1787,,Isle of Man,1.0,f +47200,67%,Costa Rica,1.0,t +25734,78%,Bosnia and Herzegovina,1.0,f +16307,100%,,2.0,f +39662,,Isle of Man,1.0,t +14120,100%,Jersey,1.0,f +36651,,Guinea,1.0,f +41719,100%,Cape Verde,4.0,f +21658,,French Guiana,1.0,f +4445,,Micronesia,1.0,f +49680,,Estonia,1.0,t +38836,100%,Lebanon,3.0,f +17801,80%,Mauritania,2.0,t +43361,100%,Maldives,1.0,f +36859,,,2.0,f +42624,,,1.0,f +39865,83%,,1.0,f +9342,,Nicaragua,1.0,f +49626,,Tanzania,2.0,t +27029,100%,Isle of Man,1.0,t +35176,100%,Chile,3.0,t +42062,,Lebanon,1.0,t +42678,,Sao Tome and Principe,1.0,t +10982,100%,,1.0,f +27563,90%,,1.0,t +11234,100%,,2.0,f +43570,,Denmark,1.0,f +11997,100%,Mauritania,2.0,t +23246,,,1.0,f +40602,,Slovenia,1.0,f +32226,30%,,1.0,f +5232,93%,,2.0,t +11009,,Niue,1.0,f +39944,,Lebanon,1.0,t +16015,,,1.0,t +26723,,,1.0,t +35118,,Bouvet Island (Bouvetoya),1.0,t +49457,,Suriname,1.0,f +5945,,French Guiana,1.0,t +4809,,Anguilla,1.0,f +30870,,,1.0,t +765,,Mauritania,1.0,t +46925,,,1.0,t +9056,,Marshall Islands,1.0,t +24146,100%,Cocos (Keeling) Islands,1.0,f +17587,80%,Peru,1.0,t +11275,100%,Brazil,3.0,t +31972,100%,,1.0,t +20382,100%,Svalbard & Jan Mayen Islands,4.0,t +10061,100%,Mauritania,1.0,t +37538,,Nicaragua,1.0,f +48863,50%,Isle of Man,1.0,f +21410,,,1.0,t +19062,,Brazil,1.0,f +17221,,,1.0,t +20511,,Vanuatu,1.0,f +25030,75%,Micronesia,1.0,t +4096,,Tunisia,1.0,f +26253,,,1.0,f +43450,,Isle of Man,1.0,f +725,33%,Peru,2.0,f +29195,,Nicaragua,1.0,f +4716,100%,Tonga,1.0,f +20466,100%,Anguilla,1.0,f +25528,100%,Svalbard & Jan Mayen Islands,1.0,f +38729,100%,Maldives,1.0,t +7000,100%,,1.0,t +46449,100%,Tonga,1.0,f +35894,100%,,1.0,f +2476,100%,Saint Helena,2.0,t +12471,67%,Gibraltar,1.0,t +28199,,Gibraltar,1.0,t +46688,91%,Vanuatu,2.0,f +26053,,Grenada,1.0,t +20517,,Kenya,1.0,f +8777,67%,,1.0,f +23662,100%,Vanuatu,3.0,f +44429,,,1.0,t +31210,100%,Cape Verde,1.0,t +33543,,,1.0,f +27345,100%,Guinea,1.0,f +11880,100%,El Salvador,1.0,f +49221,,,2.0,f +4851,0%,Chad,1.0,f +28367,100%,Marshall Islands,1.0,f +36726,94%,Mexico,6.0,f +20432,100%,,1.0,t +44451,33%,,1.0,f +20021,,Gambia,1.0,f +23680,,,1.0,t +13016,100%,Russian Federation,2.0,t +28004,100%,,1.0,t +37280,100%,Indonesia,4.0,t +5489,100%,Tonga,1.0,f +32598,,Vietnam,1.0,f +8060,,,1.0,f +19645,,Maldives,1.0,f +15433,40%,United Kingdom,1.0,f +20935,100%,Gambia,2.0,f +31916,,Malta,2.0,t +8301,,Guernsey,1.0,f +50078,100%,Chad,2.0,t +49592,,Marshall Islands,1.0,f +49124,,Marshall Islands,3.0,t +31985,,,1.0,f +35172,100%,Barbados,1.0,t +15105,,,1.0,f +33051,100%,Libyan Arab Jamahiriya,1.0,f +19784,100%,,1.0,f +9123,,Brazil,1.0,f +18856,50%,El Salvador,1.0,t +28062,,Isle of Man,1.0,f +26718,100%,Tunisia,1.0,f +20458,,Isle of Man,1.0,f +25776,75%,Micronesia,1.0,f +22986,,,1.0,f +11434,100%,,1.0,t +23739,,Uzbekistan,1.0,f +22004,,Vanuatu,1.0,t +1604,100%,Tunisia,1.0,t +41880,,China,2.0,f +11201,,Faroe Islands,1.0,t +44701,,Chad,1.0,t +16012,0%,,1.0,t +48478,100%,Marshall Islands,1.0,t +14237,,France,1.0,t +23850,88%,,1.0,f +18387,,Tonga,1.0,f +49471,100%,,2.0,t +45900,,Monaco,1.0,f +29553,100%,,1.0,t +21902,100%,Vanuatu,3.0,f +5536,,Barbados,1.0,t +48280,,,1.0,f +29587,,Cook Islands,1.0,f +3325,50%,,1.0,f +23325,100%,Russian Federation,2.0,t +18184,,,1.0,f +30357,,,1.0,t +37530,,Niue,1.0,t +7720,,Tonga,1.0,f +23258,,,1.0,f +15715,100%,,1.0,f +13444,,,1.0,f +6113,100%,Guinea,2.0,t +30067,,Niue,1.0,t +379,50%,Guinea,1.0,f +36716,100%,Bosnia and Herzegovina,3.0,f +29479,33%,,1.0,t +25134,,,1.0,t +49581,,,1.0,f +48784,,,2.0,t +24221,100%,Guinea,1.0,t +31288,100%,Isle of Man,1.0,f +37755,,Ukraine,1.0,t +27435,100%,Rwanda,1.0,t +13618,67%,,2.0,f +39551,,Nicaragua,1.0,t +29875,,,1.0,t +41590,,Cocos (Keeling) Islands,1.0,f +23695,100%,France,3.0,f +14004,100%,Kiribati,1.0,f +35236,100%,Chile,1.0,t +49159,,Montserrat,1.0,f +28317,100%,Somalia,1.0,f +165,,,1.0,f +5381,,French Guiana,1.0,t +46195,89%,Maldives,6.0,t +39612,100%,,1.0,f +20166,,Djibouti,1.0,f +37864,,Suriname,1.0,f +2309,,Bouvet Island (Bouvetoya),2.0,t +6738,100%,,2.0,f +13133,67%,Denmark,1.0,t +48421,100%,,4.0,t +15356,80%,Denmark,2.0,f +21386,100%,Isle of Man,1.0,t +8341,100%,Costa Rica,2.0,f +118,,,1.0,f +13849,100%,Niue,2.0,t +39178,,,1.0,f +14775,,United Kingdom,1.0,f +31785,,Guinea,1.0,t +5011,,Micronesia,1.0,t +22877,,,1.0,f +41931,75%,,1.0,t +4103,,,1.0,f +43774,,,1.0,f +37159,,United Kingdom,1.0,f +9861,100%,Guernsey,2.0,f +47886,100%,Turkmenistan,1.0,t +4305,100%,,2.0,f +17680,,Malta,1.0,t +6779,,Rwanda,1.0,t +18191,,Djibouti,2.0,f +11157,100%,,2.0,t +17789,94%,Reunion,6.0,f +18607,100%,Niue,1.0,f +19776,100%,Marshall Islands,2.0,t +43310,100%,Russian Federation,1.0,f +47730,100%,Saint Helena,1.0,f +33725,80%,Slovakia (Slovak Republic),1.0,f +5242,,Malta,1.0,f +27970,100%,Niue,2.0,t +16167,,,1.0,t +47406,,,1.0,f +44116,100%,Zimbabwe,1.0,t +28656,,,1.0,f +41877,,Cuba,1.0,t +45546,100%,Cape Verde,19.0,t +23276,100%,Brazil,4.0,f +14228,100%,Marshall Islands,2.0,f +28037,,China,1.0,f +39313,,Tonga,1.0,t +30302,,Chad,1.0,f +47478,,,1.0,f +35018,100%,Nicaragua,10.0,f +3439,100%,,1.0,f +28644,,Anguilla,1.0,f +23791,,,1.0,t +2285,100%,,1.0,f +12787,,Estonia,1.0,t +887,,Turkmenistan,1.0,f +6501,100%,,1.0,f +30057,,Lebanon,1.0,t +28115,92%,,1.0,f +1549,100%,Malta,1.0,t +40561,90%,Bouvet Island (Bouvetoya),1.0,t +6259,100%,Indonesia,1.0,f +37253,100%,Saint Helena,1.0,t +7054,100%,,1.0,f +12652,,Marshall Islands,2.0,f +20597,,,1.0,f +27872,,Denmark,3.0,t +46105,100%,Nicaragua,4.0,t +45394,100%,Marshall Islands,2.0,f +1922,,France,1.0,f +42107,,Fiji,1.0,f +85,100%,Russian Federation,1.0,t +47525,100%,,2.0,t +22892,100%,,1.0,t +21632,100%,Wallis and Futuna,2.0,f +23541,89%,Rwanda,1.0,f +24975,0%,,1.0,f +2934,,Sao Tome and Principe,1.0,t +40328,,Netherlands,1.0,f +15004,100%,Uzbekistan,88.0,f +4101,,Croatia,1.0,f +4840,,,1.0,f +37565,100%,Slovenia,1.0,t +27212,,,1.0,f +33961,100%,Indonesia,3.0,t +27157,,El Salvador,1.0,t +43094,,Monaco,1.0,f +48725,,Rwanda,1.0,t +28931,,Canada,1.0,t +19643,,,2.0,f +39267,,Bouvet Island (Bouvetoya),1.0,f +40626,93%,United Kingdom,5.0,t +33279,100%,,1.0,f +40934,100%,Brazil,3.0,f +13134,,,1.0,t +9901,,Tonga,1.0,f +20015,100%,Maldives,1.0,t +610,,,1.0,f +907,100%,,1.0,f +32081,,,1.0,f +23377,,Micronesia,1.0,f +6976,,Uzbekistan,2.0,f +11691,100%,,1.0,f +36081,100%,Russian Federation,1.0,f +22839,,Nauru,1.0,f +40644,,Bosnia and Herzegovina,1.0,t +12609,100%,Micronesia,1.0,t +33372,100%,Guinea,22.0,t +13545,,Papua New Guinea,1.0,t +6755,100%,Barbados,2.0,f +48196,,France,1.0,f +7382,33%,,1.0,t +18359,,Isle of Man,1.0,t +34200,,,1.0,f +3262,100%,Philippines,1.0,f +47476,,Kenya,1.0,t +35548,,Guernsey,1.0,t +37136,,,1.0,f +11148,0%,,1.0,f +10866,100%,,1.0,t +21280,,,1.0,t +16260,100%,United Kingdom,4.0,t +31273,,Faroe Islands,1.0,t +30324,,Lebanon,1.0,t +15048,,French Guiana,1.0,t +38748,43%,Chad,1.0,f +46075,,Gambia,1.0,t +38930,,Russian Federation,1.0,f +48171,100%,,1.0,f +4782,100%,,3.0,t +25243,,,1.0,f +46463,,Cocos (Keeling) Islands,1.0,f +13589,100%,China,1.0,f +7460,100%,China,1.0,f +41567,100%,Chile,1.0,t +27860,,Isle of Man,1.0,t +45860,100%,Niue,1.0,t +45391,,,1.0,f +35697,100%,,1.0,f +41326,,,2.0,t +31283,,Guinea,1.0,t +31293,100%,Uzbekistan,1.0,f +46848,100%,,1.0,f +19721,100%,Tonga,3.0,t +30892,100%,Lebanon,1.0,f +7743,100%,,1.0,f +37923,,Andorra,1.0,f +34236,100%,,1.0,f +11552,50%,France,1.0,t +36302,100%,Vanuatu,1.0,f +42654,100%,,1.0,t +17513,,Faroe Islands,1.0,t +12195,,Mexico,1.0,f +37807,,Jersey,1.0,f +26782,,Congo,1.0,f +27747,,Malta,1.0,f +20948,,Denmark,1.0,t +46634,100%,,1.0,t +13085,100%,Guinea,1.0,t +22635,,,1.0,t +41354,100%,Micronesia,4.0,t +17139,,Tanzania,1.0,t +25344,90%,Faroe Islands,8.0,f +36928,,Rwanda,1.0,f +17868,100%,Niue,2.0,f +6270,100%,Monaco,1.0,f +45041,100%,Niue,2.0,f +26063,,Croatia,2.0,t +10441,100%,,2.0,f +36689,100%,Turkmenistan,1.0,t +35584,100%,Lebanon,1.0,f +34947,,,1.0,f +23566,,Denmark,1.0,f +921,100%,Finland,1.0,f +26807,,Zimbabwe,1.0,f +49042,,Uzbekistan,1.0,t +21018,,,1.0,t +46134,100%,,1.0,t +8189,100%,Chile,2.0,t +42054,100%,Guinea,2.0,f +14425,,Gibraltar,2.0,t +6430,100%,Niue,2.0,t +4705,,China,2.0,t +33571,,,1.0,t +20456,100%,,1.0,f +5732,,Ghana,2.0,t +38921,100%,Montserrat,1.0,f +23842,,Palestinian Territory,1.0,f +26461,,Monaco,1.0,f +21511,,,1.0,f +2424,100%,Indonesia,1.0,f +14885,100%,Tanzania,3.0,f +16836,,French Guiana,1.0,t +33187,,Lebanon,1.0,t +5105,,Niue,1.0,f +10204,100%,Nicaragua,5.0,t +26052,90%,Gambia,3.0,t +16527,100%,Palestinian Territory,2.0,f +20987,100%,,1.0,f +9934,100%,,2.0,f +34302,,,2.0,t +11348,,China,2.0,f +43622,,Maldives,1.0,f +18378,100%,Tonga,1.0,t +11321,,,1.0,f +28331,100%,,3.0,f +34972,,Greenland,4.0,f +29246,50%,Malawi,1.0,t +6481,,Guinea,2.0,f +25801,,Lebanon,1.0,t +3365,100%,,1.0,t +6471,100%,Zimbabwe,1.0,t +47505,,,2.0,t +44753,100%,,1.0,f +20847,100%,Bouvet Island (Bouvetoya),2.0,t +9077,,,1.0,f +15272,100%,,2.0,t +10732,,Uganda,5.0,f +2216,100%,,1.0,f +26460,,Nicaragua,1.0,f +8699,100%,Niue,1.0,t +8810,,,1.0,t +7052,,Bouvet Island (Bouvetoya),1.0,f +20020,100%,Sao Tome and Principe,2.0,t +10891,,,1.0,f +21232,90%,Kiribati,2.0,t +20122,,Philippines,1.0,t +47436,,El Salvador,1.0,t +20926,,Sao Tome and Principe,1.0,f +14498,,China,1.0,f +33101,100%,Nauru,2.0,f +43040,,,8.0,t +43980,,Croatia,1.0,t +37286,100%,,2.0,f +38011,,,1.0,f +46910,,,1.0,t +5680,0%,,1.0,f +37174,,Kenya,1.0,f +36502,70%,,2.0,f +12257,,Isle of Man,1.0,f +39248,97%,Cuba,5.0,f +32603,83%,Nicaragua,4.0,t +42159,,,1.0,f +36788,,,3.0,f +23751,100%,Afghanistan,1.0,f +40532,100%,,1.0,t +34992,,Croatia,1.0,f +42155,,Uzbekistan,1.0,f +36860,,Russian Federation,2.0,f +10726,100%,,1.0,f +24444,,Estonia,1.0,f +49703,100%,Nicaragua,9.0,f +46035,100%,United Kingdom,1.0,f +31721,50%,,1.0,f +29083,0%,Chile,1.0,t +18563,,Jersey,1.0,t +22468,,,1.0,f +11002,,Philippines,10.0,f +5679,100%,Maldives,1.0,f +20860,,Togo,1.0,f +39217,,,1.0,f +30173,,,1.0,t +47371,,Guinea,1.0,f +22047,100%,,1.0,t +49251,100%,Uzbekistan,1.0,f +31314,100%,Guinea,4.0,t +12450,,Malawi,1.0,t +43362,100%,,1.0,f +38903,25%,,1.0,f +35030,,Kenya,1.0,f +34392,,,1.0,f +11021,,Slovakia (Slovak Republic),1.0,t +41836,100%,Tonga,1.0,f +43156,100%,Papua New Guinea,1.0,t +20939,,Niue,1.0,t +33286,,Chad,1.0,t +28785,100%,Lebanon,1.0,f +48004,100%,,1.0,f +15316,100%,Russian Federation,1.0,t +44294,,,1.0,t +23248,100%,Sao Tome and Principe,1.0,f +43966,,,1.0,f +3691,,Djibouti,1.0,f +1550,100%,El Salvador,1.0,t +15531,,Monaco,1.0,t +12927,100%,Russian Federation,1.0,t +4411,,,1.0,f +47068,,Tunisia,1.0,f +3817,0%,,1.0,f +37631,,Russian Federation,1.0,t +4139,67%,Guernsey,1.0,f +39120,,Malta,1.0,t +40059,83%,Svalbard & Jan Mayen Islands,1.0,t +13049,100%,,1.0,t +17431,,Kenya,1.0,f +18424,,,1.0,f +26238,100%,Turkmenistan,2.0,t +19,100%,Reunion,2.0,f +10539,,Lebanon,1.0,f +6292,100%,,1.0,t +43680,38%,Ukraine,1.0,f +36363,82%,Marshall Islands,5.0,f +32875,0%,Kenya,1.0,t +20229,100%,Papua New Guinea,1.0,f +28953,56%,,2.0,f +29324,86%,Russian Federation,2.0,f +11604,100%,,1.0,f +23882,100%,,1.0,f +45948,,Gambia,1.0,f +17703,100%,Marshall Islands,1.0,f +24279,,Sao Tome and Principe,1.0,t +41374,,,1.0,f +7940,100%,Niue,1.0,t +15598,,,1.0,t +43668,100%,Marshall Islands,6.0,t +889,,Russian Federation,1.0,f +3515,,Cook Islands,1.0,f +15886,,Sao Tome and Principe,1.0,t +4274,100%,Guernsey,2.0,t +36013,100%,,1.0,t +10433,,Rwanda,1.0,f +5296,100%,Mauritania,4.0,f +44737,,El Salvador,2.0,f +3552,0%,Kiribati,1.0,f +12783,,Micronesia,1.0,f +28742,,,1.0,f +3932,100%,Uzbekistan,2.0,f +23583,,Ghana,1.0,t +21696,67%,Bouvet Island (Bouvetoya),2.0,t +43006,,,1.0,f +33514,100%,Marshall Islands,2.0,t +27553,100%,,1.0,t +44256,,Nicaragua,1.0,t +24665,100%,,1.0,f +20352,,,1.0,t +16526,,Niue,2.0,t +26020,,Palestinian Territory,1.0,f +20310,100%,Andorra,1.0,f +41988,,Costa Rica,1.0,f +3372,,Maldives,1.0,f +47442,,Zimbabwe,1.0,f +41257,100%,Monaco,1.0,f +9301,,,1.0,f +36899,75%,Saint Helena,2.0,t +6184,,Nicaragua,1.0,f +5136,100%,Cape Verde,1.0,t +15246,,Mexico,1.0,t +16076,,,1.0,t +1403,100%,Somalia,1.0,t +31060,,Isle of Man,1.0,t +44659,100%,Pakistan,1.0,f +27074,,Russian Federation,1.0,t +22414,90%,,1.0,f +39638,,Afghanistan,1.0,f +30279,,Kenya,1.0,f +49324,,,1.0,f +4695,,,1.0,t +23465,100%,Tonga,1.0,f +601,,Cocos (Keeling) Islands,2.0,f +7251,100%,Turks and Caicos Islands,3.0,t +11284,100%,,1.0,f +17095,,,1.0,t +43394,,Russian Federation,1.0,f +6810,,,1.0,f +47239,100%,,2.0,f +1527,,,1.0,t +24285,100%,Bouvet Island (Bouvetoya),1.0,f +25347,100%,,1.0,f +35262,,Lebanon,1.0,t +15842,,Isle of Man,1.0,f +33793,100%,Russian Federation,2.0,f +9688,100%,Slovakia (Slovak Republic),1.0,t +35345,,Zimbabwe,1.0,t +43748,,France,1.0,t +27492,100%,Nicaragua,2.0,t +15046,,Jersey,1.0,f +27041,50%,Russian Federation,1.0,f +9757,,Denmark,2.0,f +42524,,Nauru,1.0,t +3112,94%,Indonesia,25.0,f +49212,,Somalia,1.0,t +15281,,Bosnia and Herzegovina,1.0,f +490,100%,Bosnia and Herzegovina,6.0,f +36748,,Ghana,3.0,f +4271,,Turks and Caicos Islands,1.0,f +20525,100%,,1.0,t +15929,,Rwanda,1.0,f +48788,90%,,1.0,f +12877,,Croatia,1.0,t +36384,100%,,1.0,f +48305,100%,Guinea,2.0,t +21371,,Guinea,2.0,f +3916,100%,Guernsey,1.0,t +27357,,Russian Federation,1.0,t +25744,90%,Mauritania,1.0,f +39889,,,1.0,f +10906,,Chad,1.0,f +47836,,,1.0,t +41667,100%,,2.0,t +7062,,Chile,1.0,t +21155,100%,Libyan Arab Jamahiriya,1.0,f +6526,,Lebanon,2.0,t +31631,,Tonga,1.0,t +20236,100%,Isle of Man,1.0,t +6959,95%,Slovakia (Slovak Republic),20.0,t +8537,,Bouvet Island (Bouvetoya),1.0,f +46851,100%,Gibraltar,1.0,t +23678,,,1.0,t +19112,,Malta,1.0,f +37281,,,1.0,t +50086,100%,,1.0,f +17726,,Lebanon,1.0,t +24178,100%,,1.0,f +39263,100%,Rwanda,2.0,t +11789,100%,Niue,1.0,f +4857,,,1.0,f +9568,,,1.0,f +39294,,Russian Federation,1.0,t +2217,,,9.0,f +47420,,Nauru,1.0,t +42726,100%,Niue,1.0,f +46321,,,1.0,f +19028,,Turkmenistan,1.0,t +33911,100%,Kiribati,2.0,t +17969,100%,Russian Federation,1.0,f +41762,100%,Russian Federation,1.0,f +47204,,,1.0,t +20130,,United Kingdom,1.0,f +2725,100%,,3.0,t +17010,100%,United Kingdom,2.0,t +12591,100%,Maldives,4.0,t +45205,100%,Denmark,1.0,f +25210,,Russian Federation,2.0,f +46305,,,1.0,f +45905,100%,Sao Tome and Principe,3.0,t +38394,,,1.0,f +20970,,China,1.0,f +2034,,Lithuania,2.0,t +28496,100%,Nicaragua,38.0,f +46019,100%,,1.0,t +40631,80%,,1.0,t +49290,,,1.0,f +13088,100%,Marshall Islands,1.0,f +42362,,,1.0,f +35962,,Mexico,1.0,f +39388,0%,,2.0,t +2232,100%,Russian Federation,1.0,f +3607,50%,Faroe Islands,1.0,f +27773,100%,Togo,1.0,f +41580,100%,Turkmenistan,1.0,f +14161,100%,Montserrat,2.0,f +11968,100%,,3.0,f +14490,100%,Denmark,1.0,t +34586,75%,,1.0,t +27964,,Russian Federation,1.0,f +24073,100%,Zimbabwe,2.0,f +49292,,Denmark,1.0,f +46530,90%,Malawi,1.0,t +4252,100%,,1.0,f +12371,,Cape Verde,1.0,t +6020,100%,Isle of Man,1.0,t +36784,,,1.0,f +40483,50%,Niue,1.0,f +4735,100%,,2.0,f +42819,100%,Lebanon,2.0,f +21917,100%,,1.0,f +10266,,Senegal,1.0,f +35295,100%,Andorra,1.0,f +26536,,Denmark,1.0,f +28041,,Guernsey,1.0,f +32066,,,1.0,f +2833,100%,Cuba,1.0,t +16137,100%,Gibraltar,1.0,f +39821,,,1.0,t +15981,,Slovakia (Slovak Republic),1.0,f +15474,,Sao Tome and Principe,1.0,t +40114,,,1.0,f +16678,,Micronesia,1.0,f +43180,,,1.0,f +2175,,Uganda,1.0,t +42934,,Faroe Islands,1.0,t +34514,,Lebanon,1.0,t +38087,100%,Niger,1.0,f +41473,,Niue,2.0,t +44955,90%,,1.0,f +16060,,,1.0,f +22162,,,1.0,f +28506,,Marshall Islands,1.0,f +18164,100%,Marshall Islands,5.0,f +10844,,,1.0,f +40883,,Reunion,1.0,f +42753,,Bosnia and Herzegovina,1.0,t +28740,100%,,1.0,f +36260,100%,Papua New Guinea,1.0,f +28469,100%,Vanuatu,3.0,t +33060,,,1.0,f +4282,,Maldives,2.0,t +31102,,,1.0,t +28848,100%,Uganda,9.0,f +21083,100%,Spain,3.0,t +33902,,Libyan Arab Jamahiriya,1.0,f +17632,71%,Afghanistan,1.0,t +8546,,San Marino,1.0,t +29927,100%,,2.0,f +3713,0%,El Salvador,1.0,f +11833,90%,Afghanistan,2.0,f +20337,45%,Tonga,1.0,f +28502,100%,Niue,1.0,t +14424,100%,,1.0,f +15711,,China,1.0,f +34209,,Philippines,1.0,f +24888,100%,Estonia,1.0,t +9154,100%,Djibouti,2.0,f +2204,,,1.0,f +33440,88%,Isle of Man,3.0,f +47547,100%,,1.0,f +33605,100%,Zimbabwe,1.0,t +45590,100%,Turkmenistan,2.0,t +1454,,,1.0,t +37908,100%,Niue,1.0,t +16500,100%,France,1.0,f +13160,100%,Cape Verde,5.0,t +34354,100%,,1.0,t +44990,100%,Marshall Islands,1.0,f +14261,,Greenland,1.0,f +11732,,Tanzania,1.0,t +32836,100%,Isle of Man,1.0,f +13815,100%,Malta,1.0,t +35161,,Anguilla,1.0,f +24529,,Maldives,2.0,f +25543,89%,Zimbabwe,1.0,t +24703,,Tanzania,1.0,t +16025,,Switzerland,1.0,f +16966,,Denmark,2.0,t +35655,100%,Niue,1.0,f +26504,,Niue,1.0,f +42261,100%,Niue,1.0,f +15878,100%,Reunion,1.0,f +23795,100%,Indonesia,1.0,f +48047,,,1.0,t +14455,100%,,2.0,f +15020,100%,Denmark,1.0,f +222,89%,,16.0,f +49653,40%,Marshall Islands,1.0,t +34722,100%,,1.0,t +12165,,,1.0,f +26809,100%,,8.0,f +17553,100%,Maldives,2.0,t +24126,100%,Turkmenistan,1.0,t +29422,50%,Uzbekistan,1.0,f +11772,,Guinea,1.0,f +27240,,,1.0,t +26089,100%,Jersey,1.0,t +43691,,Anguilla,1.0,t +32319,70%,,1.0,t +38775,,,1.0,f +249,,Nauru,1.0,f +1980,50%,,1.0,t +13595,,,1.0,f +5751,,Brazil,1.0,f +4250,100%,,2.0,f +19519,,,1.0,f +6524,,,1.0,f +15193,100%,Tanzania,2.0,t +25611,,Estonia,1.0,f +47890,100%,,1.0,f +16476,86%,Micronesia,1.0,f +16179,100%,,1.0,f +4041,,Russian Federation,1.0,f +3961,100%,Fiji,8.0,f +48291,,Bosnia and Herzegovina,2.0,f +2080,0%,Malta,1.0,t +15282,100%,Nicaragua,1.0,t +35827,,Kiribati,1.0,f +40759,,Venezuela,1.0,f +9233,,Greenland,1.0,f +37430,,,1.0,f +34500,100%,Chad,1.0,t +44401,100%,United Kingdom,1.0,t +29975,,,1.0,f +39210,100%,,1.0,f +48106,100%,Cocos (Keeling) Islands,1.0,f +47471,100%,Lithuania,1.0,f +11105,100%,,1.0,f +33469,,,1.0,f +48982,,Portugal,1.0,f +44547,100%,Venezuela,1.0,f +9797,100%,Uzbekistan,1.0,f +31465,100%,,1.0,f +6735,,Isle of Man,1.0,t +41073,,Wallis and Futuna,1.0,t +22191,100%,Cape Verde,2.0,f +9460,100%,Kiribati,1.0,f +20515,100%,Niue,2.0,t +25197,100%,Nicaragua,4.0,f +27033,94%,,1.0,t +14186,,Palestinian Territory,2.0,f +8125,,Monaco,1.0,f +11612,100%,Kiribati,1.0,f +15278,,Isle of Man,1.0,t +8553,100%,,1.0,f +19807,100%,United Kingdom,1.0,f +16685,100%,Reunion,2.0,f +47383,0%,Marshall Islands,1.0,f +11018,,,1.0,f +34539,,Kenya,1.0,f +47441,100%,,1.0,f +16295,96%,Philippines,5.0,f +5387,,,1.0,f +36206,86%,Russian Federation,10.0,f +10883,100%,Niger,8.0,t +42945,100%,,1.0,f +48786,90%,Pakistan,1.0,f +8302,,Costa Rica,1.0,f +21303,,Nicaragua,1.0,t +17896,,Canada,1.0,f +31691,,Puerto Rico,1.0,f +49706,,Cocos (Keeling) Islands,1.0,t +41061,,Maldives,1.0,t +8695,,Papua New Guinea,1.0,f +18795,,Maldives,1.0,f +6423,,Malta,2.0,t +36772,100%,Lebanon,1.0,f +24531,,Turkmenistan,1.0,f +48849,,Denmark,1.0,f +26611,100%,Papua New Guinea,2.0,f +4093,100%,,1.0,t +37166,56%,,1.0,t +16485,,Bosnia and Herzegovina,1.0,f +28721,100%,French Guiana,1.0,t +41271,100%,,2.0,t +22543,100%,,1.0,t +25662,100%,Bouvet Island (Bouvetoya),1.0,f +27024,,,1.0,f +26080,,,1.0,t +4403,,Turkmenistan,1.0,f +1783,100%,Tunisia,2.0,t +5441,89%,,1.0,t +45117,,Brazil,1.0,t +46531,,Niue,2.0,f +35134,,Ecuador,2.0,f +7804,100%,Russian Federation,3.0,f +24173,,,1.0,f +40918,,,1.0,t +39514,,Philippines,1.0,t +38431,,Russian Federation,1.0,t +42543,,,1.0,f +3494,,,1.0,f +20579,100%,,1.0,f +49341,100%,Papua New Guinea,1.0,t +17216,100%,Ecuador,2.0,t +31983,100%,French Guiana,1.0,f +42750,,Guinea,1.0,t +15170,100%,Guinea,1.0,f +1159,100%,Monaco,1.0,t +13281,,,1.0,f +43675,70%,Micronesia,2.0,f +2183,100%,Isle of Man,3.0,f +28780,100%,Russian Federation,1.0,t +1962,100%,,7.0,f +12584,,Philippines,1.0,t +12014,100%,Pakistan,1.0,f +41230,100%,Togo,2.0,f +34650,,Cook Islands,1.0,f +45425,100%,Mexico,1.0,t +29712,,Rwanda,1.0,f +30821,,Niger,1.0,t +14579,100%,Uzbekistan,1.0,t +2466,,Solomon Islands,1.0,f +28175,,,1.0,f +5418,,Philippines,1.0,f +28047,100%,Uzbekistan,4.0,f +35929,,Guinea,1.0,f +27179,100%,,1.0,f +43648,,Croatia,1.0,f +45557,100%,,2.0,f +4286,100%,,1.0,f +15344,100%,Svalbard & Jan Mayen Islands,1.0,f +37491,100%,Lebanon,1.0,t +8533,,Bouvet Island (Bouvetoya),1.0,f +5816,,Rwanda,4.0,f +42203,100%,Afghanistan,1.0,t +5878,100%,,2.0,t +32148,,Nicaragua,1.0,t +9621,,,1.0,t +1756,,Croatia,1.0,f +476,,Guernsey,1.0,t +34620,100%,Costa Rica,1.0,t +34585,100%,,2.0,t +5112,,Lithuania,1.0,f +36688,100%,Lithuania,1.0,f +1961,100%,,1.0,f +11057,100%,,1.0,t +42790,,Marshall Islands,1.0,t +684,,Bouvet Island (Bouvetoya),1.0,f +49077,100%,,1.0,t +37901,,Micronesia,1.0,t +41679,,Bosnia and Herzegovina,1.0,t +35164,,Chad,1.0,f +34125,,,1.0,t +20842,,Lebanon,1.0,t +19176,,Greenland,1.0,f +46071,89%,Niue,2.0,f +15304,100%,Korea,1.0,f +43798,100%,El Salvador,4.0,t +2739,,,1.0,f +5721,100%,Togo,1.0,f +30737,88%,,1.0,f +12983,100%,Micronesia,1.0,f +44837,,Niue,1.0,t +48897,100%,Russian Federation,1.0,t +15226,100%,French Polynesia,1.0,t +47949,100%,,1.0,t +27252,100%,Russian Federation,1.0,f +38315,100%,,1.0,t +4873,,France,1.0,f +41316,,Ghana,3.0,f +16229,,Pakistan,3.0,f +19847,100%,Maldives,1.0,f +3763,100%,Mauritania,1.0,f +4881,,Kenya,1.0,f +50098,,Marshall Islands,2.0,f +4517,100%,El Salvador,3.0,t +26728,,,1.0,f +26704,100%,Malawi,2.0,t +44391,100%,Kiribati,2.0,f +40817,,Lebanon,1.0,f +43719,,China,1.0,f +32839,,,1.0,f +44696,,,1.0,f +23088,100%,El Salvador,1.0,t +40317,100%,Uzbekistan,2.0,t +7374,100%,,1.0,f +25144,,Lebanon,1.0,f +32394,33%,,1.0,f +39037,,Isle of Man,2.0,t +39699,100%,Vanuatu,1.0,f +48650,,Guernsey,1.0,f +32267,100%,,1.0,t +31141,,Niger,1.0,t +48470,90%,Pakistan,4.0,f +28146,,Vanuatu,1.0,t +9800,,,1.0,f +46024,100%,,1.0,t +41208,,Anguilla,1.0,f +47993,100%,Malawi,1.0,t +35696,,,3.0,t +29180,,,1.0,t +6684,,Sao Tome and Principe,1.0,f +22229,100%,Afghanistan,3.0,t +21294,,,1.0,t +31479,,Barbados,1.0,f +398,,Uzbekistan,1.0,t +8015,100%,Denmark,1.0,t +28078,100%,Maldives,1.0,f +22029,100%,,1.0,f +16328,100%,,1.0,f +1020,100%,United Kingdom,7.0,t +40150,100%,Philippines,1.0,f +24766,,,1.0,t +37040,,Tonga,1.0,f +283,100%,United Kingdom,1.0,t +9607,,Montserrat,1.0,t +35417,,,1.0,t +35598,100%,France,2.0,f +15758,100%,Togo,1.0,f +6781,100%,Tanzania,1.0,f +25895,,French Guiana,1.0,f +11720,,Finland,1.0,f +7039,,Uganda,2.0,f +34064,,Christmas Island,3.0,t +7884,,Montserrat,1.0,f +4795,100%,Malta,1.0,f +46939,71%,,1.0,t +45764,100%,Guernsey,1.0,t +26261,100%,Turks and Caicos Islands,1.0,f +30570,,Chile,1.0,t +25106,,Costa Rica,2.0,t +13138,100%,Faroe Islands,2.0,t +46240,100%,Nicaragua,1.0,t +29547,,Nicaragua,1.0,f +34195,100%,,1.0,t +1054,100%,Guinea,20.0,f +43643,,Zimbabwe,1.0,f +13546,100%,Rwanda,15.0,f +49876,,Kiribati,1.0,f +44653,,Niue,1.0,f +2612,94%,Cuba,1.0,f +9218,,Denmark,1.0,f +1817,,Lithuania,1.0,f +39341,,,1.0,t +15716,,Maldives,1.0,f +29688,100%,French Guiana,1.0,t +6571,90%,,1.0,t +2982,,,1.0,t +24203,,,2.0,t +12402,67%,,1.0,t +15427,100%,Malta,3.0,f +44457,,,1.0,f +48084,,Niger,3.0,f +8030,,Russian Federation,1.0,f +9999,100%,Isle of Man,1.0,f +18912,100%,Gibraltar,1.0,f +29898,100%,Jersey,1.0,f +13458,,Lebanon,1.0,t +14859,,Turkmenistan,1.0,t +28583,,Marshall Islands,1.0,t +19781,,,1.0,t +33857,,,1.0,t +30443,,,1.0,f +21230,100%,Lithuania,1.0,f +49068,100%,Gibraltar,1.0,t +168,,Portugal,1.0,f +24478,,,1.0,t +4183,100%,,1.0,f +9354,100%,Spain,12.0,f +15904,100%,Afghanistan,2.0,f +34288,100%,Gibraltar,1.0,f +14010,100%,Tonga,1.0,t +48979,,Switzerland,2.0,f +46431,,,1.0,f +6932,,,1.0,t +32778,,,1.0,t +304,100%,,1.0,f +49851,,Ukraine,1.0,f +10809,100%,Bosnia and Herzegovina,4.0,f +2403,50%,Gibraltar,2.0,f +39565,93%,Rwanda,12.0,f +22584,,,1.0,f +16828,60%,,1.0,f +15160,100%,Peru,5.0,t +35097,,Costa Rica,1.0,t +10740,100%,Indonesia,1.0,f +15387,,Russian Federation,1.0,t +30967,,Sao Tome and Principe,1.0,t +28410,,Cocos (Keeling) Islands,1.0,f +12978,100%,,1.0,f +17926,,Faroe Islands,1.0,f +4370,,,1.0,f +16407,,Kiribati,1.0,t +9549,100%,,1.0,f +34482,100%,Montserrat,1.0,t +42174,,Faroe Islands,1.0,f +34044,,,1.0,t +38641,,Mauritania,1.0,f +16739,,,2.0,f +41521,,Monaco,1.0,f +5398,100%,,1.0,f +45313,,,1.0,f +18012,100%,,2.0,t +10111,,Anguilla,1.0,f +40044,100%,Marshall Islands,1.0,f +17890,100%,France,1.0,f +6512,100%,,1.0,f +22479,,,1.0,f +1303,100%,,1.0,t +27926,,Bosnia and Herzegovina,2.0,f +13021,,,1.0,t +3892,,,1.0,t +27795,,Nicaragua,1.0,f +26061,,Philippines,1.0,t +15850,,French Guiana,1.0,f +6436,100%,,1.0,f +33082,,,1.0,t +651,100%,Lebanon,4.0,t +25385,,Korea,1.0,t +13131,,Rwanda,1.0,f +28290,,Philippines,1.0,f +49227,,,1.0,t +22989,,China,2.0,t +11081,,Faroe Islands,2.0,f +11421,,,1.0,f +24264,,Kiribati,1.0,t +21178,100%,,1.0,t +6373,100%,,2.0,f +3065,100%,Tunisia,1.0,t +28294,100%,Puerto Rico,1.0,f +41177,100%,Ecuador,6.0,t +46033,100%,,1.0,t +33835,,Monaco,1.0,t +8545,100%,,1.0,f +11121,,,1.0,t +7078,100%,Fiji,1.0,f +24951,,,1.0,f +44650,67%,Jersey,1.0,f +36977,78%,,2.0,f +30337,100%,,1.0,t +30112,,Uzbekistan,1.0,t +17200,,,1.0,t +21613,,,1.0,t +43877,,Chad,1.0,t +32210,0%,,1.0,f +14356,,Papua New Guinea,1.0,f +23765,,,1.0,f +33611,,Canada,1.0,t +29136,86%,Svalbard & Jan Mayen Islands,1.0,f +33029,100%,Isle of Man,3.0,f +23669,100%,,1.0,f +10542,,Pakistan,1.0,f +43689,0%,,1.0,f +16106,100%,French Guiana,1.0,t +36526,100%,Russian Federation,1.0,f +6727,100%,Kiribati,2.0,t +12950,100%,,3.0,f +31731,100%,,1.0,f +32195,100%,Chile,1.0,f +49396,,Greenland,1.0,f +22936,,Sao Tome and Principe,1.0,t +8179,100%,,1.0,f +2310,,Croatia,1.0,t +15873,100%,,1.0,t +7016,0%,Papua New Guinea,1.0,f +9345,100%,Bosnia and Herzegovina,4.0,f +29854,100%,Russian Federation,1.0,t +42394,100%,Marshall Islands,2.0,f +11486,100%,El Salvador,2.0,f +8623,100%,Jersey,1.0,f +34109,,,1.0,t +2921,,Cuba,2.0,t +38263,,Indonesia,1.0,t +14677,,Marshall Islands,1.0,t +29054,100%,Montserrat,1.0,t +11952,100%,,1.0,f +12116,,Uganda,1.0,f +31651,86%,Lebanon,2.0,t +19015,100%,Sri Lanka,2.0,f +47166,100%,Chad,1.0,t +17780,,,2.0,f +34084,100%,Gibraltar,1.0,f +28781,,Anguilla,2.0,f +30898,100%,,1.0,t +15332,100%,Anguilla,9.0,f +39221,,,1.0,f +28343,100%,Costa Rica,1.0,f +49623,100%,Faroe Islands,1.0,f +33958,100%,,1.0,f +29173,,Afghanistan,1.0,t +36835,,Bouvet Island (Bouvetoya),1.0,f +8851,,,1.0,t +22078,,Indonesia,20.0,f +48501,,,1.0,t +1911,,Venezuela,1.0,t +43011,100%,Wallis and Futuna,6.0,f +9852,100%,Maldives,11.0,f +1564,100%,Indonesia,12.0,f +27732,100%,Puerto Rico,3.0,t +31896,86%,Niue,1.0,t +29348,,Maldives,1.0,t +46192,,,1.0,t +15606,,French Guiana,1.0,f +4077,,Niue,1.0,t +47182,,,1.0,f +40364,100%,Tonga,25.0,f +13322,50%,Anguilla,2.0,f +14392,,Malta,1.0,f +39126,100%,Guinea,9.0,f +2568,50%,Gibraltar,1.0,f +48880,100%,France,1.0,f +24694,,Sao Tome and Principe,1.0,f +11219,100%,,1.0,f +44926,,Gibraltar,1.0,f +9484,100%,,1.0,f +25503,100%,Gambia,1.0,t +38584,100%,,1.0,t +34573,,,1.0,f +962,,Reunion,4.0,t +48111,,,1.0,t +779,,,2.0,f +24872,,Malta,2.0,f +15451,,Bouvet Island (Bouvetoya),1.0,f +42908,90%,,2.0,f +9749,100%,Marshall Islands,1.0,t +8842,100%,Slovakia (Slovak Republic),3.0,f +15501,,,1.0,t +25948,100%,Slovakia (Slovak Republic),1.0,t +7565,,Kiribati,1.0,f +33609,,,1.0,f +42250,100%,,2.0,t +6318,100%,,1.0,f +43705,,,1.0,f +4182,0%,France,1.0,f +32346,100%,United Kingdom,3.0,f +31993,100%,Holy See (Vatican City State),1.0,t +44750,,,1.0,t +29686,,Sao Tome and Principe,1.0,t +38378,90%,Guinea,1.0,f +24651,,Kiribati,4.0,f +44519,,Cocos (Keeling) Islands,1.0,f +37043,100%,Pakistan,11.0,f +40106,,,1.0,t +21559,75%,Tokelau,3.0,t +32792,,Indonesia,12.0,f +25010,,,2.0,f +32612,,Tanzania,1.0,t +7143,,El Salvador,1.0,f +4366,100%,Vanuatu,1.0,f +47540,75%,,1.0,t +2901,100%,Turkmenistan,1.0,t +49560,100%,,6.0,t +34099,100%,Micronesia,1.0,t +37634,,Uzbekistan,1.0,t +30953,100%,Uzbekistan,1.0,t +32659,100%,Cuba,1.0,t +19078,,Croatia,2.0,f +25850,,Chad,1.0,t +35357,,Chad,1.0,t +5678,,,1.0,t +32716,,Micronesia,1.0,t +32360,100%,Niue,1.0,t +3548,0%,,1.0,f +24144,,,2.0,t +511,,,1.0,t +4190,100%,United Kingdom,2.0,f +40345,,Tonga,1.0,f +10268,,,4.0,t +30466,,,1.0,t +20155,100%,,2.0,t +32666,,Russian Federation,1.0,t +35906,,Greenland,1.0,t +38424,,,1.0,f +26956,,,1.0,t +18423,,Anguilla,1.0,f +42803,,,2.0,f +4502,,Costa Rica,1.0,f +494,,Anguilla,1.0,f +10702,,Turkmenistan,1.0,f +9895,100%,,1.0,t +24492,,Niue,1.0,t +944,,Vietnam,7.0,t +2412,60%,,1.0,f +10738,,,1.0,f +34232,,,1.0,f +41340,100%,Tanzania,3.0,t +28828,100%,Isle of Man,198.0,t +45701,100%,Nauru,2.0,f +45941,100%,United Kingdom,1.0,t +17976,100%,,1.0,f +16273,,Lebanon,1.0,t +10392,,Chad,1.0,f +6473,90%,Uzbekistan,4.0,t +19759,86%,Cocos (Keeling) Islands,1.0,f +45028,100%,Malta,1.0,t +44193,80%,Croatia,1.0,f +21968,,,1.0,f +8483,100%,Peru,2.0,f +42260,100%,Tanzania,1.0,f +29541,100%,Palestinian Territory,1.0,f +6100,,Jersey,1.0,f +30767,100%,,1.0,f +12518,,Faroe Islands,1.0,t +45610,100%,Sao Tome and Principe,1.0,t +18309,,,1.0,f +2293,100%,Zimbabwe,3.0,t +34016,100%,Maldives,1.0,t +8418,,Denmark,2.0,f +46537,,Malawi,1.0,f +9587,100%,,1.0,f +8167,,Croatia,1.0,f +44806,,,1.0,t +44708,100%,Gibraltar,1.0,f +45252,,,1.0,f +18591,100%,Brazil,1.0,f +27407,50%,Micronesia,1.0,f +48930,,Bosnia and Herzegovina,1.0,f +11567,100%,Niger,31.0,t +19590,,Sao Tome and Principe,1.0,t +12390,86%,Uzbekistan,3.0,f +27774,78%,Vietnam,15.0,f +30007,,Niue,2.0,t +40707,100%,Sao Tome and Principe,1.0,t +39181,100%,Niger,1.0,f +40223,100%,,1.0,f +16047,80%,Uzbekistan,6.0,f +28436,100%,Anguilla,1.0,t +33602,52%,Russian Federation,2.0,t +45702,,,1.0,t +3050,,Nauru,1.0,f +14242,,,1.0,f +12826,100%,,2.0,f +47076,,Zimbabwe,1.0,t +49048,100%,Russian Federation,1.0,t +17274,,France,1.0,f +47840,,Somalia,1.0,t +32981,100%,,1.0,f +9791,80%,Rwanda,2.0,t +40589,100%,,1.0,t +14294,,,1.0,f +17545,,Afghanistan,1.0,f +49749,,,1.0,t +4322,97%,Tonga,32.0,f +50042,,Russian Federation,1.0,f +49015,,Lebanon,1.0,t +24578,100%,Jersey,1.0,t +3040,,,1.0,t +39691,,,2.0,f +27109,,Uganda,2.0,t +5902,100%,Nicaragua,1.0,f +443,,,1.0,t +44350,,Montserrat,1.0,f +36261,90%,Slovakia (Slovak Republic),2.0,f +26652,97%,Pakistan,6.0,f +29388,,Kiribati,1.0,f +21831,100%,,1.0,t +4928,50%,Malta,1.0,f +4022,50%,,1.0,t +23324,,Bosnia and Herzegovina,1.0,t +13461,,,1.0,f +32838,,Croatia,1.0,f +33522,,Tanzania,1.0,t +38198,,Bouvet Island (Bouvetoya),2.0,f +45413,,Montserrat,1.0,f +42596,100%,Brazil,1.0,f +46799,,Maldives,1.0,f +26787,100%,,1.0,f +37101,,Isle of Man,1.0,f +27023,,Sao Tome and Principe,1.0,t +20851,,,1.0,t +579,0%,Anguilla,1.0,f +12798,100%,,1.0,t +24236,60%,,1.0,t +11723,,,1.0,f +27972,94%,Brazil,27.0,f +26363,,,1.0,f +43449,,Costa Rica,1.0,t +2264,80%,,1.0,f +28911,,,1.0,f +26976,,Mauritania,1.0,t +7732,100%,Guinea,1.0,t +21487,100%,Tanzania,1.0,f +46200,100%,,2.0,f +44716,,Marshall Islands,1.0,f +3237,100%,Turkmenistan,2.0,t +41739,,Russian Federation,1.0,t +40590,,,1.0,f +40768,100%,Anguilla,1.0,t +19141,,Indonesia,1.0,t +17511,88%,Niue,1.0,f +36520,,Maldives,1.0,f +2651,,,1.0,f +48095,100%,,1.0,f +23937,,,1.0,f +27162,100%,Isle of Man,1.0,f +15314,,Christmas Island,1.0,f +45668,100%,Malta,2.0,f +2393,100%,Pakistan,1.0,f +19562,,,1.0,f +38367,,Costa Rica,1.0,t +400,,Lebanon,1.0,t +49551,100%,Maldives,1.0,f +47843,33%,,1.0,f +38021,100%,Bahrain,2.0,f +42471,100%,Fiji,1.0,f +18994,,Isle of Man,1.0,f +30990,50%,,1.0,t +24728,100%,Turkmenistan,3.0,t +21892,100%,Micronesia,1.0,f +22816,100%,Croatia,1.0,f +6247,,Nicaragua,1.0,t +6191,100%,Uzbekistan,2.0,t +25181,,Mauritania,1.0,t +43138,100%,Cocos (Keeling) Islands,1.0,t +24261,,,1.0,f +9867,100%,Guernsey,1.0,t +42548,100%,Lithuania,2.0,t +30954,100%,Niue,1.0,f +16553,,Cocos (Keeling) Islands,1.0,t +42891,100%,,1.0,f +40349,,,1.0,f +43796,100%,Niue,1.0,f +27246,100%,,3.0,f +45629,,,2.0,f +19919,75%,,1.0,f +28212,,,1.0,f +13756,100%,Guinea,3.0,f +37091,75%,Anguilla,1.0,f +44149,,,1.0,t +38917,38%,,2.0,t +41992,,,1.0,t +11078,100%,,1.0,t +48127,,Micronesia,1.0,t +30735,,,2.0,f +12273,,,1.0,t +28636,80%,United Kingdom,2.0,t +34641,,Svalbard & Jan Mayen Islands,1.0,f +47865,80%,,4.0,t +9394,,Nicaragua,1.0,f +38742,,Malta,1.0,f +42523,60%,,1.0,t +39015,,Montserrat,1.0,f +1539,100%,Micronesia,3.0,f +3942,100%,Svalbard & Jan Mayen Islands,1.0,f +8383,,Chad,1.0,f +28103,,,1.0,t +47046,100%,Lebanon,9.0,f +44758,,,1.0,f +1924,,Russian Federation,2.0,f +1846,,Croatia,1.0,t +34894,,Russian Federation,1.0,t +40555,100%,,1.0,t +31103,,Netherlands,1.0,t +8016,100%,French Guiana,1.0,t +30495,,Indonesia,1.0,t +32325,,Croatia,1.0,f +25524,100%,Barbados,1.0,t +46566,,,1.0,f +8868,,Nauru,1.0,f +32954,,,1.0,f +945,,,1.0,f +4806,,Tonga,1.0,t +12741,83%,,1.0,t +33235,70%,France,2.0,t +20891,100%,Niue,2.0,t +46148,100%,Uganda,1.0,f +22093,100%,,1.0,f +37495,,,1.0,t +41852,,,1.0,f +37980,75%,Kiribati,1.0,f +27163,100%,Korea,3.0,t +18038,,,1.0,f +13904,,,1.0,f +4175,100%,Gambia,2.0,t +34018,100%,French Guiana,1.0,f +16492,67%,Marshall Islands,1.0,f +23865,90%,,2.0,t +705,100%,,1.0,f +35527,100%,Marshall Islands,13.0,f +22285,100%,Rwanda,1.0,f +37012,100%,Monaco,3.0,t +28084,100%,,1.0,f +1232,100%,,21.0,f +11428,,Kenya,1.0,f +22631,,,1.0,f +47090,100%,Croatia,3.0,f +14402,100%,Denmark,1.0,f +25699,,Isle of Man,1.0,t +48764,,Kiribati,1.0,f +20281,100%,,1.0,f +23869,,,1.0,f +48916,100%,Libyan Arab Jamahiriya,1.0,f +34731,100%,,1.0,t +12897,,,1.0,f +30149,,Malta,1.0,t +12432,,,2.0,f +6550,,Micronesia,1.0,f +30787,100%,,2.0,t +21430,,,1.0,t +29116,100%,El Salvador,19.0,t +43555,,Peru,1.0,f +29778,100%,Chile,1.0,f +42616,,Pakistan,4.0,f +34869,83%,,1.0,t +34048,,Nicaragua,1.0,f +8398,100%,Kenya,1.0,f +7888,,,1.0,t +25706,100%,Isle of Man,1.0,f +8728,100%,Ecuador,1.0,f +5644,100%,,1.0,t +30431,,,1.0,t +2282,,,1.0,f +44853,100%,Lebanon,2.0,f +26898,100%,,1.0,f +410,100%,Bouvet Island (Bouvetoya),1.0,f +33083,,,1.0,t +38281,,Korea,1.0,t +41047,,,1.0,t +31724,100%,Bouvet Island (Bouvetoya),1.0,f +26635,0%,Tanzania,1.0,f +3122,100%,Peru,1.0,f +26403,,Mauritania,1.0,f +10457,,,2.0,t +48471,,,1.0,t +45042,,Guernsey,1.0,t +22169,100%,,3.0,f +38916,,Uzbekistan,1.0,f +16741,,Russian Federation,1.0,t +28834,,El Salvador,1.0,f +34206,100%,,1.0,f +24620,,Uzbekistan,1.0,f +34289,90%,Sao Tome and Principe,1.0,t +1740,,Niue,1.0,t +3827,,,1.0,t +36268,,,1.0,f +47113,,Faroe Islands,2.0,f +10276,,Jersey,1.0,f +8729,89%,Svalbard & Jan Mayen Islands,1.0,f +26783,60%,,2.0,f +27556,,Russian Federation,1.0,f +22827,,,1.0,t +7055,100%,,1.0,f +14822,,Chile,1.0,f +32446,33%,,2.0,f +7179,,Maldives,3.0,f +71,,Lithuania,2.0,t +48559,,,1.0,f +3422,86%,Niger,6.0,f +34660,100%,Reunion,1.0,f +34768,,Chad,1.0,t +35569,,Marshall Islands,1.0,f +47127,,Palestinian Territory,1.0,f +5570,100%,Finland,1.0,t +39642,,,2.0,f +16705,,Micronesia,1.0,f +34884,100%,,1.0,t +47697,,Guinea,1.0,f +46832,100%,Niue,2.0,t +42470,100%,Malawi,2.0,f +45345,,Sao Tome and Principe,1.0,t +32843,,,1.0,f +31195,,Russian Federation,1.0,f +11810,,French Guiana,1.0,t +49189,100%,Turks and Caicos Islands,3.0,f +30428,100%,Turkmenistan,1.0,t +46354,100%,Papua New Guinea,1.0,f +40869,100%,Jersey,1.0,t +28231,,,1.0,t +48960,100%,,13.0,t +2994,100%,El Salvador,1.0,t +6225,90%,Barbados,7.0,f +27473,100%,,2.0,f +10185,,Mexico,1.0,t +42856,,,1.0,t +29131,,Suriname,1.0,f +45082,,Monaco,2.0,t +49796,67%,,1.0,f +42566,,,1.0,f +32867,,,1.0,f +3120,,Sao Tome and Principe,1.0,f +46949,100%,Tunisia,4.0,f +44838,,Tonga,1.0,f +37524,100%,Lebanon,1.0,f +36219,100%,Brazil,1.0,t +44763,,Niue,1.0,f +16415,,Lebanon,1.0,f +14499,,Monaco,3.0,f +35219,97%,Tonga,4.0,f +24575,,Portugal,1.0,f +18318,,,1.0,t +15266,100%,Bouvet Island (Bouvetoya),1.0,f +891,100%,Chile,2.0,t +2417,,Cocos (Keeling) Islands,1.0,f +27231,100%,Isle of Man,1.0,t +20832,,Kiribati,1.0,f +8758,100%,Russian Federation,1.0,f +46332,100%,Isle of Man,1.0,f +13686,,France,3.0,f +24227,100%,,1.0,f +38696,100%,Slovakia (Slovak Republic),1.0,f +9458,100%,Reunion,2.0,f +45870,100%,Puerto Rico,2.0,t +5231,100%,Nicaragua,1.0,f +1003,100%,Tonga,1.0,t +18993,100%,Reunion,2.0,t +25148,50%,Uzbekistan,1.0,t +31637,100%,Estonia,1.0,f +12044,100%,Switzerland,2.0,f +27418,,Grenada,1.0,f +46536,,,1.0,t +18238,,,2.0,t +46443,75%,Sao Tome and Principe,1.0,t +44459,89%,Kenya,1.0,t +8951,100%,Faroe Islands,1.0,f +49597,,Bahrain,1.0,t +37104,100%,Bouvet Island (Bouvetoya),1.0,t +16245,100%,,1.0,f +6856,100%,Denmark,1.0,f +16994,,French Guiana,1.0,t +25649,,Suriname,1.0,t +26593,33%,,1.0,f +19409,100%,Micronesia,1.0,f +26265,,Venezuela,1.0,t +26755,100%,Russian Federation,2.0,t +45193,100%,,1.0,f +40652,,,1.0,t +47382,,Uganda,1.0,f +41761,100%,,1.0,f +42176,,,1.0,t +47848,30%,Uzbekistan,1.0,t +22574,,,1.0,f +37014,,,1.0,f +35135,100%,Niue,1.0,f +7751,,,1.0,t +15636,,Uzbekistan,2.0,f +23981,100%,Nicaragua,1.0,f +49556,,French Guiana,1.0,t +2036,100%,Slovakia (Slovak Republic),3.0,f +23332,100%,Bosnia and Herzegovina,2.0,f +43540,72%,Bosnia and Herzegovina,25.0,f +23400,,,1.0,t +657,100%,Marshall Islands,2.0,f +24090,,Niue,1.0,f +1260,,Lebanon,1.0,f +28989,,El Salvador,1.0,t +27233,100%,Zimbabwe,1.0,t +40308,,Niue,1.0,f +20726,0%,Niue,1.0,t +3989,100%,Marshall Islands,1.0,t +22036,100%,Mauritania,1.0,t +17406,,Anguilla,1.0,f +19699,,Svalbard & Jan Mayen Islands,1.0,f +33450,100%,Jersey,1.0,t +16501,100%,,1.0,f +11844,75%,Saint Helena,1.0,t +48968,,Croatia,1.0,t +30336,100%,Vietnam,2.0,t +26067,60%,Reunion,1.0,f +49174,,,1.0,f +38009,,Nicaragua,1.0,t +26521,100%,Vanuatu,1.0,f +22564,90%,Portugal,1.0,f +31310,100%,Kiribati,1.0,t +20931,100%,,1.0,f +20037,,Kenya,2.0,t +10210,,,1.0,f +49184,,,1.0,t +43951,,,1.0,f +45690,,Nauru,2.0,f +4130,,Maldives,7.0,f +17087,50%,Portugal,1.0,t +19284,,Vietnam,1.0,f +44237,100%,Andorra,1.0,t +27428,100%,Uzbekistan,2.0,f +8032,70%,United Kingdom,1.0,t +20375,80%,Mauritania,1.0,f +36083,,Bosnia and Herzegovina,1.0,f +16213,,,1.0,t +21791,,Gambia,1.0,f +18781,,Gambia,1.0,f +4043,100%,,1.0,t +22410,,,1.0,f +11877,,Maldives,1.0,t +9169,90%,Guernsey,1.0,f +36306,,Brazil,1.0,t +9954,,,1.0,t +36403,100%,United Kingdom,1.0,t +35141,100%,,4.0,f +13577,90%,Brazil,2.0,f +25085,,Niue,1.0,t +36225,,Marshall Islands,3.0,f +377,,Turks and Caicos Islands,1.0,f +16026,,,1.0,f +34865,,Uganda,2.0,t +39230,,Nauru,1.0,f +212,100%,,1.0,f +14039,100%,Sao Tome and Principe,1.0,f +38127,90%,,2.0,f +16386,100%,Uzbekistan,1.0,t +38391,,,1.0,f +19539,100%,,1.0,f +4275,,,1.0,t +2929,,,1.0,f +48107,100%,Uganda,1.0,f +44128,100%,,2.0,t +31563,100%,Niue,3.0,f +23166,50%,Faroe Islands,1.0,t +17168,,,1.0,f +8123,,,1.0,f +17349,,Lebanon,1.0,t +20154,,Chad,1.0,t +48272,,French Guiana,1.0,t +38244,,,1.0,f +22742,,Barbados,1.0,f +6407,0%,Niue,3.0,f +23665,100%,,1.0,t +49665,100%,Libyan Arab Jamahiriya,1.0,f +26923,,Lebanon,1.0,t +6829,,Ecuador,1.0,f +36241,100%,,1.0,f +7233,,Maldives,1.0,t +49113,83%,Barbados,1.0,t +21333,100%,Faroe Islands,2.0,f +49012,100%,Micronesia,1.0,t +49950,100%,,1.0,f +44793,100%,Guernsey,1.0,f +37370,,Russian Federation,1.0,t +39416,,,1.0,f +12,100%,,1.0,t +34977,,Finland,1.0,f +6832,100%,Monaco,1.0,f +49652,,,1.0,t +808,,Venezuela,1.0,f +191,,,1.0,t +18620,,Faroe Islands,1.0,f +23341,,Croatia,2.0,f +39625,100%,Zimbabwe,1.0,t +41470,,Saint Helena,1.0,t +19570,0%,Greenland,1.0,f +8683,,Guinea,1.0,f +39721,,Tunisia,1.0,f +6038,,,1.0,f +19599,,Russian Federation,1.0,f +26046,100%,Namibia,2.0,f +12784,100%,Malta,1.0,f +10496,,Faroe Islands,1.0,t +18465,,Korea,1.0,t +25251,100%,El Salvador,2.0,f +49992,,,1.0,f +44029,,,1.0,t +30360,0%,Moldova,1.0,f +1199,100%,Spain,1.0,f +9824,93%,,1.0,f +32354,,Isle of Man,1.0,f +25249,0%,,1.0,f +40722,,Maldives,2.0,f +43620,100%,Denmark,1.0,f +10741,100%,Micronesia,2.0,f +46108,,Monaco,1.0,f +24062,,Estonia,1.0,f +49258,,,1.0,f +50035,100%,Tonga,1.0,f +30712,100%,Faroe Islands,1.0,f +20718,,Holy See (Vatican City State),1.0,f +3804,100%,Faroe Islands,1.0,f +22634,100%,Gambia,3.0,f +5501,,Marshall Islands,1.0,t +23374,0%,Togo,2.0,t +10358,,Micronesia,2.0,f +21489,100%,,1.0,f +48144,,China,1.0,t +38494,100%,Palestinian Territory,1.0,f +8688,,Brazil,2.0,f +32964,,,1.0,t +26887,,,2.0,f +4450,,,1.0,t +11085,,Malta,1.0,t +712,,Niue,1.0,f +49446,,Kiribati,2.0,f +13527,,Russian Federation,3.0,t +17135,0%,,1.0,t +41595,,Uzbekistan,1.0,f +20748,,,1.0,f +30997,,,1.0,t +9226,,Mauritania,5.0,f +17083,,Estonia,1.0,t +4640,,,1.0,t +49904,100%,Senegal,1.0,f +40954,0%,Uganda,1.0,f +30550,,,1.0,f +18825,,,2.0,t +36030,,Croatia,2.0,f +20409,90%,,2.0,f +29258,100%,Barbados,1.0,f +48538,,,1.0,t +30949,100%,Finland,1.0,f +10427,100%,,1.0,t +23320,,Togo,2.0,f +36585,,Philippines,1.0,f +24098,,,1.0,f +11671,100%,,1.0,f +9204,11%,Russian Federation,1.0,t +10131,,Chad,1.0,f +7154,,,1.0,f +13258,100%,Micronesia,1.0,f +2301,100%,,1.0,f +44045,100%,Sao Tome and Principe,2.0,f +15394,100%,Tunisia,2.0,t +30560,,,1.0,f +37459,100%,,4.0,f +22453,,Malta,1.0,f +20350,100%,,1.0,f +14337,,,1.0,f +210,,Libyan Arab Jamahiriya,1.0,t +10384,,Malta,1.0,f +35661,,,1.0,f +12830,,Niue,1.0,t +15693,100%,Vietnam,6.0,f +41341,100%,Guinea,3.0,t +6383,,Djibouti,1.0,t +45739,80%,Maldives,6.0,t +48441,83%,,2.0,f +27967,67%,Djibouti,1.0,t +9310,70%,Gambia,1.0,f +12868,100%,Tonga,1.0,f +38659,,Niue,1.0,t +46549,,French Guiana,1.0,f +2734,,,1.0,f +46565,,Isle of Man,1.0,f +41028,100%,Micronesia,1.0,f +33748,30%,Uzbekistan,3.0,t +28528,,,1.0,f +226,100%,Maldives,2.0,f +21889,,Vietnam,1.0,t +16808,100%,Niue,1.0,f +12069,100%,,1.0,f +48879,100%,Russian Federation,1.0,t +46994,,Malta,1.0,f +1587,,Gambia,1.0,t +18139,100%,,3.0,f +4512,,,1.0,t +34891,,Lebanon,1.0,f +49063,,,1.0,t +4657,100%,,1.0,f +998,60%,Niue,1.0,t +8288,,,1.0,f +2225,,Micronesia,1.0,f +33332,100%,Puerto Rico,1.0,t +19123,,,1.0,f +34974,100%,,2.0,t +20776,,Micronesia,1.0,f +44678,,Russian Federation,1.0,f +22894,100%,,1.0,f +12103,,,1.0,f +45750,100%,,1.0,t +20709,100%,,1.0,f +13556,100%,Uzbekistan,1.0,f +35757,,Gambia,1.0,t +37626,,Micronesia,3.0,f +23482,100%,Niue,1.0,f +44973,100%,Kiribati,1.0,f +8626,,Estonia,1.0,f +17501,100%,Cape Verde,1.0,f +227,,Jersey,1.0,f +17686,,Reunion,2.0,t +48050,100%,,2.0,t +34201,,Uzbekistan,1.0,f +24533,,,1.0,t +17258,,,1.0,f +30187,100%,Marshall Islands,2.0,f +22605,100%,,1.0,f +2004,,Svalbard & Jan Mayen Islands,1.0,f +19934,100%,Svalbard & Jan Mayen Islands,2.0,t +40459,,Estonia,1.0,f +14989,,,1.0,t +18836,100%,,1.0,f +9539,,,1.0,t +8215,,,1.0,t +38831,65%,Tanzania,8.0,t +15656,90%,,2.0,t +20279,100%,,1.0,t +44486,100%,,2.0,f +42345,,Peru,1.0,f +45783,100%,Nauru,1.0,t +17058,100%,,1.0,t +48634,,Maldives,1.0,t +4710,25%,Guernsey,1.0,t +8625,,,1.0,t +6654,100%,Tonga,3.0,f +40956,,Kenya,1.0,t +38070,,Saint Helena,1.0,f +2415,75%,,1.0,t +46770,,Malawi,1.0,t +24876,,Russian Federation,1.0,f +19901,,Guinea,1.0,f +9590,100%,,3.0,f +19107,,Zimbabwe,1.0,t +46279,,Faroe Islands,1.0,t +2262,100%,Tonga,1.0,f +2821,,,1.0,t +7301,,Sao Tome and Principe,1.0,t +20399,,,1.0,f +4178,100%,Niue,1.0,t +34313,,Marshall Islands,1.0,f +11950,,Gambia,1.0,t +33182,,Cook Islands,1.0,f +34995,,Kenya,1.0,f +12546,100%,Lebanon,1.0,f +36320,100%,Niue,1.0,f +37267,,Ghana,1.0,t +25179,0%,Zimbabwe,1.0,f +34252,,,1.0,f +29768,100%,Uganda,1.0,f +15958,,,1.0,f +10554,0%,Russian Federation,2.0,f +38514,,Kenya,1.0,f +1582,100%,Russian Federation,1.0,f +44770,90%,,1.0,t +28408,100%,Australia,1.0,f +45453,100%,,1.0,f +26604,100%,Turkmenistan,1.0,f +30172,100%,Russian Federation,12.0,f +18679,100%,Andorra,1.0,f +710,100%,Kenya,1.0,t +47514,100%,Philippines,1.0,f +41117,,Chad,2.0,t +33956,100%,Lebanon,1.0,t +46756,,Guernsey,1.0,f +8592,100%,Uganda,1.0,f +15299,,,1.0,t +34547,,Gibraltar,1.0,f +10696,,El Salvador,1.0,t +24463,,,3.0,f +9633,100%,Uruguay,1.0,f +7524,,,1.0,f +42329,,El Salvador,1.0,f +10765,,Monaco,3.0,f +2686,,,1.0,f +25159,100%,Peru,1.0,t +46689,,,1.0,t +8389,,,1.0,f +40334,,Bosnia and Herzegovina,1.0,t +27522,,Chile,1.0,f +9256,,Ghana,1.0,f +4945,0%,Cape Verde,5.0,f +30924,,Sao Tome and Principe,7.0,t +2515,100%,France,1.0,t +43949,,Montserrat,1.0,f +49381,,Malta,1.0,t +31504,,Croatia,3.0,f +43590,100%,Costa Rica,9.0,f +17115,100%,Gambia,2.0,t +26883,100%,Guernsey,2.0,t +33865,,,1.0,f +25549,100%,Guinea,1.0,f +28903,,Anguilla,1.0,f +38520,100%,Malta,1.0,t +49423,,,1.0,f +10818,,Cape Verde,1.0,f +41376,,,1.0,f +29552,100%,Jersey,1.0,f +41728,,Turkmenistan,1.0,f +46156,,Sao Tome and Principe,1.0,t +4115,100%,,2.0,f +46893,100%,,1.0,f +24556,100%,Zimbabwe,1.0,t +45513,,Russian Federation,1.0,f +15535,100%,Niue,1.0,f +4353,,Niue,1.0,f +24834,100%,Maldives,1.0,t +35193,100%,Kiribati,1.0,f +18717,100%,,1.0,t +8253,,Nicaragua,4.0,f +44982,50%,Isle of Man,2.0,t +15790,0%,,1.0,f +21794,,Canada,1.0,f +7254,88%,Tonga,1.0,t +48041,100%,Cocos (Keeling) Islands,1.0,t +17383,100%,,1.0,f +21354,,Kenya,1.0,f +19614,100%,Tonga,5.0,t +39239,100%,,1.0,f +32213,,Puerto Rico,1.0,f +20228,100%,,1.0,f +4748,,Niger,1.0,f +36896,50%,Turkmenistan,1.0,f +32516,100%,,1.0,f +25639,,Lebanon,1.0,f +45658,,,1.0,f +26399,,Marshall Islands,1.0,f +40090,100%,Portugal,1.0,f +26495,75%,Palestinian Territory,1.0,t +6145,100%,Russian Federation,2.0,t +49433,,,1.0,t +25049,100%,,2.0,f +35791,,Montserrat,2.0,t +1094,80%,,1.0,f +45188,100%,,1.0,t +14459,40%,Ukraine,1.0,f +5874,100%,Guinea,1.0,f +36466,,Marshall Islands,1.0,f +46504,100%,Barbados,1.0,f +33106,100%,Nicaragua,1.0,t +19073,100%,,1.0,t +48257,100%,Kiribati,5.0,t +41734,,,1.0,t +2680,,,1.0,t +40041,,Tuvalu,3.0,t +35380,,Chad,1.0,f +12542,98%,Mauritania,19.0,t +41447,0%,Andorra,3.0,f +47147,,Anguilla,1.0,f +41913,,Andorra,1.0,f +47129,,,1.0,t +46951,,,1.0,t +17199,,,1.0,f +40461,,,1.0,t +42355,,Russian Federation,2.0,t +43283,,Sao Tome and Principe,1.0,f +27036,100%,Marshall Islands,1.0,t +22402,,China,3.0,t +47474,,Lebanon,1.0,f +38163,100%,Venezuela,3.0,f +30435,99%,Cape Verde,41.0,f +39833,,Djibouti,1.0,t +25698,100%,Monaco,1.0,f +7413,100%,Cape Verde,1.0,t +35522,100%,Kiribati,10.0,f +39488,,,1.0,t +27171,100%,Reunion,1.0,t +8100,,Netherlands,1.0,t +8714,100%,,2.0,f +7994,100%,,1.0,f +16113,100%,Croatia,2.0,t +29884,100%,,3.0,f +48599,100%,Malawi,3.0,f +14657,,Reunion,2.0,t +39111,,Gibraltar,1.0,f +22929,,Isle of Man,1.0,f +37207,80%,Tonga,1.0,f +28901,,Maldives,1.0,f +35893,,Senegal,1.0,t +34525,100%,Estonia,1.0,f +48804,,Nicaragua,1.0,f +22621,,,1.0,t +36307,,Bouvet Island (Bouvetoya),1.0,f +25045,,Reunion,1.0,f +14882,,,1.0,f +45592,100%,Maldives,2.0,t +7516,100%,,1.0,f +49865,,Costa Rica,1.0,t +19976,,Cape Verde,1.0,t +39582,,,1.0,f +38172,,Chad,1.0,t +2019,100%,Malta,7.0,f +15219,,Guinea,1.0,t +37179,,China,1.0,f +15959,100%,Russian Federation,2.0,t +29416,100%,Anguilla,1.0,f +12613,,Gibraltar,2.0,t +48525,,Afghanistan,3.0,f +37662,100%,Kenya,2.0,t +6003,,,1.0,f +46435,,,1.0,f +6721,100%,,2.0,f +36072,,Guinea,1.0,f +3705,,,1.0,f +29277,100%,France,1.0,f +8331,70%,Uganda,2.0,f +49863,33%,,1.0,f +40660,,,1.0,t +37194,,,2.0,f +2614,,,1.0,t +23239,,,1.0,t +48348,100%,United Kingdom,1.0,t +13154,,Cuba,1.0,t +7670,100%,Isle of Man,1.0,t +37632,100%,Costa Rica,2.0,f +41113,,Congo,1.0,t +4238,88%,,1.0,f +33488,,Kenya,1.0,f +17650,100%,Tanzania,1.0,f +26720,,Kenya,3.0,f +12887,,,1.0,f +28520,100%,Slovakia (Slovak Republic),2.0,t +31880,100%,,1.0,f +24685,100%,,1.0,f +41612,100%,Montserrat,1.0,f +1341,,Sao Tome and Principe,1.0,t +42688,,China,1.0,f +8912,,Lebanon,4.0,f +17227,,Micronesia,1.0,f +48481,,,1.0,t +19327,90%,Mauritania,1.0,f +1280,100%,,1.0,f +4253,100%,France,1.0,t +24860,,,1.0,f +10222,100%,,4.0,t +21488,,,1.0,f +15548,100%,Faroe Islands,1.0,t +30769,,Zimbabwe,1.0,t +35338,80%,,1.0,f +28950,100%,Mexico,2.0,t +3571,100%,Bouvet Island (Bouvetoya),3.0,t +14486,,Anguilla,1.0,t +13873,50%,,2.0,t +16700,,Svalbard & Jan Mayen Islands,1.0,f +23426,,,1.0,t +33699,100%,Palestinian Territory,1.0,f +2104,100%,Russian Federation,1.0,t +15689,96%,Micronesia,1.0,f +33583,,Costa Rica,1.0,f +21482,100%,Russian Federation,1.0,t +21639,,Korea,1.0,t +24476,,,1.0,f +14127,,Chad,1.0,t +39353,100%,Lithuania,2.0,t +46844,100%,Sao Tome and Principe,1.0,f +9817,100%,,1.0,f +26904,100%,Uganda,1.0,t +16003,100%,,1.0,f +48031,,Chad,1.0,t +9211,,,2.0,t +47210,,Papua New Guinea,1.0,f +12219,,Jersey,1.0,f +33734,,,1.0,f +6966,100%,Afghanistan,1.0,t +16750,,Estonia,1.0,f +31611,,,1.0,f +10173,83%,Marshall Islands,2.0,t +3443,100%,Jersey,1.0,f +44544,0%,Gibraltar,2.0,f +1568,,Costa Rica,1.0,t +1445,,Somalia,1.0,t +7635,100%,Kenya,1.0,t +46218,100%,,2.0,f +44178,,,1.0,f +30286,100%,Montserrat,39.0,f +31058,100%,Afghanistan,1.0,t +45119,,Rwanda,2.0,f +21503,83%,,1.0,f +21323,100%,Guinea,2.0,t +48954,100%,Montserrat,8.0,t +22981,100%,,1.0,f +23367,,United Kingdom,1.0,t +11387,100%,Uganda,2.0,f +2046,100%,Niue,1.0,f +41212,,,1.0,t +9697,,Turks and Caicos Islands,1.0,t +42971,100%,Niue,1.0,f +1223,,Cape Verde,1.0,t +26434,100%,Tonga,1.0,f +44964,,Lebanon,1.0,t +37304,,,1.0,f +25898,,,1.0,t +3952,,Lithuania,1.0,t +39594,,Sao Tome and Principe,3.0,f +42117,100%,,1.0,f +34076,100%,Niue,7.0,t +35997,,Monaco,2.0,f +41644,,Isle of Man,1.0,t +12874,100%,,1.0,t +29520,,Nicaragua,54.0,f +36506,70%,Marshall Islands,2.0,f +16799,,Rwanda,1.0,t +34933,,,1.0,t +21706,100%,,1.0,t +33400,97%,Zimbabwe,5.0,t +32824,100%,Indonesia,1.0,t +633,,Denmark,1.0,f +16587,,Cocos (Keeling) Islands,2.0,f +49197,90%,,3.0,f +44026,,Afghanistan,1.0,f +46160,,Micronesia,2.0,f +43699,,Chad,1.0,t +3088,100%,Jersey,1.0,f +18580,,,1.0,t +30551,,Afghanistan,2.0,t +15840,,Marshall Islands,1.0,t +714,100%,Zimbabwe,1.0,f +8074,100%,,1.0,t +14577,50%,Niue,2.0,t +34663,100%,French Guiana,1.0,t +12128,100%,Djibouti,1.0,f +29193,83%,,1.0,t +33821,,Djibouti,1.0,f +3549,,,1.0,f +27719,89%,,4.0,f +33243,,French Guiana,1.0,f +46125,,Uzbekistan,1.0,f +40999,100%,Chile,2.0,f +10400,100%,Paraguay,4.0,t +15861,100%,Chad,1.0,t +42233,100%,,1.0,f +6647,100%,Senegal,2.0,t +9849,100%,Isle of Man,2.0,f +14358,100%,Maldives,9.0,f +33786,,,1.0,f +45261,,Malta,2.0,f +38082,,Ukraine,1.0,t +49492,100%,Jersey,1.0,t +22247,100%,Slovakia (Slovak Republic),5.0,t +42813,,Jersey,1.0,f +3274,86%,Barbados,24.0,f +27666,100%,,1.0,f +8154,100%,,4.0,f +9798,,Mauritania,2.0,t +45497,,,1.0,f +35846,,Niue,1.0,f +30224,100%,Cuba,1.0,t +43819,100%,Grenada,2.0,t +20495,100%,Anguilla,1.0,f +3641,,,1.0,f +27143,,,1.0,f +29421,,Isle of Man,1.0,t +8621,,,1.0,f +24953,100%,,1.0,t +42980,,Cook Islands,1.0,f +49855,,Togo,2.0,f +16504,100%,,1.0,t +18445,70%,,2.0,t +4598,,Nicaragua,1.0,f +17388,100%,,2.0,t +11112,,,1.0,t +29578,100%,Chile,2.0,f +39149,100%,Peru,1.0,f +10626,,,1.0,t +10585,100%,Niue,1.0,t +15117,100%,,1.0,f +42145,,Andorra,1.0,f +36872,,,1.0,t +25674,70%,Cocos (Keeling) Islands,1.0,f +15898,,,1.0,t +41879,,Anguilla,1.0,f +16195,,,1.0,t +40066,,Croatia,1.0,f +41108,,Uganda,1.0,f +19382,100%,,1.0,t +17303,100%,Russian Federation,5.0,t +37688,100%,,1.0,f +36348,,China,1.0,f +36477,,Kenya,1.0,f +32433,,Puerto Rico,1.0,t +45521,100%,Isle of Man,1.0,f +41233,100%,Zimbabwe,1.0,t +45092,,French Guiana,1.0,f +16624,100%,Chile,1.0,t +3566,90%,Turkmenistan,1.0,t +23972,100%,Zimbabwe,2.0,t +45433,,Chad,1.0,t +17339,67%,Niue,1.0,f +43841,,,1.0,f +649,,,1.0,t +13064,100%,,1.0,f +36747,,Cocos (Keeling) Islands,2.0,f +42838,0%,,2.0,f +27671,100%,Turkmenistan,1.0,f +19291,,Maldives,1.0,t +14739,,Lebanon,1.0,t +8644,,,1.0,f +10235,,Reunion,1.0,t +40390,100%,Marshall Islands,2.0,t +8852,,,1.0,t +34176,,Mauritania,1.0,f +40188,,Mexico,1.0,f +16683,,,1.0,f +37167,,Nicaragua,2.0,f +34658,,,1.0,f +26106,,Tonga,1.0,t +8198,100%,,5.0,f +21938,0%,Lithuania,1.0,f +15023,100%,Tanzania,1.0,t +1477,,,1.0,f +46701,,,1.0,t +28271,,Gambia,2.0,f +242,100%,Denmark,1.0,f +28145,,Tonga,1.0,f +40620,100%,Guinea,2.0,f +14959,,,1.0,t +1419,100%,Maldives,1.0,t +30519,100%,,1.0,t +39508,100%,Jersey,1.0,t +53,,Bahrain,1.0,t +2706,100%,,1.0,f +45979,100%,Guernsey,1.0,f +16733,100%,Vanuatu,1.0,f +40557,100%,Kenya,2.0,f +9647,,Philippines,1.0,f +48955,100%,Somalia,1.0,f +10828,,Anguilla,1.0,f +5812,,,1.0,f +46675,100%,Tonga,1.0,f +42863,,,1.0,t +20390,,Niue,1.0,t +16267,100%,Montserrat,1.0,f +16558,,Philippines,1.0,t +19839,,,1.0,f +43582,100%,,1.0,t +2899,,,1.0,t +12215,100%,,2.0,f +5403,,,1.0,f +5629,50%,Sao Tome and Principe,1.0,f +6700,100%,Guinea,7.0,f +3187,100%,Tonga,2.0,t +42968,,Afghanistan,1.0,f +32738,100%,Vanuatu,1.0,f +15957,100%,Nicaragua,1.0,f +3541,,Gibraltar,1.0,f +47709,,Nauru,1.0,t +21792,,Palestinian Territory,1.0,f +24978,100%,Christmas Island,1.0,t +26905,50%,Puerto Rico,2.0,f +47530,,,1.0,t +30101,,Andorra,1.0,t +9611,93%,Chad,2.0,f +37052,,French Guiana,1.0,f +34480,100%,,1.0,f +41137,0%,,1.0,f +38078,100%,Uzbekistan,2.0,t +15834,,,1.0,t +34285,100%,Russian Federation,1.0,t +43379,,Palestinian Territory,1.0,f +39109,77%,Ukraine,24.0,t +47991,100%,Niue,1.0,f +10404,,,1.0,f +17109,,Marshall Islands,1.0,t +38041,75%,,2.0,t +24963,100%,China,1.0,f +33844,,Jersey,1.0,f +42881,,Lebanon,1.0,t +35654,100%,Russian Federation,1.0,t +17444,100%,Malawi,1.0,f +16528,,Zimbabwe,1.0,t +32391,,Slovakia (Slovak Republic),1.0,t +38159,100%,,1.0,f +1035,,,1.0,t +22347,100%,Brazil,2.0,t +19297,100%,,1.0,t +33463,,,1.0,f +27532,,,1.0,t +8045,,Russian Federation,1.0,t +12821,100%,Tonga,1.0,t +17673,,Anguilla,1.0,f +2257,,Gambia,1.0,f +46595,,Maldives,1.0,f +43942,100%,Reunion,1.0,t +32913,,,1.0,t +30311,,Niue,2.0,f +40380,,,2.0,t +10775,100%,Guinea,2.0,t +7033,100%,Vietnam,1.0,t +5377,,,1.0,f +38492,100%,Palestinian Territory,1.0,f +48218,100%,Lebanon,1.0,t +31869,,Tonga,1.0,f +12506,,Ghana,3.0,t +12598,100%,Montserrat,3.0,f +34172,,Jersey,1.0,f +42951,75%,Marshall Islands,1.0,f +1778,,,1.0,f +20836,,,1.0,f +45059,,Gambia,1.0,f +1671,100%,Holy See (Vatican City State),1.0,t +5891,100%,Faroe Islands,1.0,t +8319,100%,,1.0,f +11936,,,1.0,t +46630,,Bahrain,1.0,f +19737,100%,,1.0,f +22272,,,1.0,t +24840,,France,1.0,f +4263,,Bahrain,1.0,t +30460,80%,Netherlands,1.0,f +28980,,Nauru,1.0,t +10774,,Turks and Caicos Islands,1.0,t +33963,,French Guiana,1.0,t +13606,100%,,4.0,f +24743,,Kenya,2.0,f +43104,,,1.0,f +46362,100%,,1.0,f +6397,,Niue,1.0,t +42280,100%,Micronesia,1.0,t +2121,67%,Kenya,2.0,f +28324,,,1.0,f +23527,,,1.0,f +43097,93%,Sao Tome and Principe,4.0,f +22020,,,1.0,f +30873,,,1.0,f +28543,,,1.0,t +12836,,,1.0,t +43344,60%,Maldives,5.0,f +12703,,Ecuador,1.0,f +20741,,Uganda,1.0,f +48705,,Guernsey,1.0,f +10157,0%,Tonga,2.0,f +39711,100%,Estonia,1.0,f +17178,25%,Rwanda,1.0,t +37371,100%,,2.0,f +22876,90%,Marshall Islands,2.0,t +1236,100%,Nauru,1.0,t +18168,95%,Ecuador,3.0,f +24234,,Micronesia,1.0,t +14393,100%,Indonesia,1.0,t +17371,,,1.0,f +26379,,Niue,1.0,f +19244,100%,,1.0,f +18288,90%,,1.0,f +10574,,Anguilla,1.0,f +41186,100%,Lebanon,1.0,t +35372,100%,,1.0,f +41771,100%,Turks and Caicos Islands,2.0,f +44234,90%,Philippines,1.0,t +29703,100%,Niue,1.0,f +22603,100%,,1.0,f +8003,100%,Uganda,1.0,f +48450,100%,Cape Verde,2.0,t +4032,80%,,1.0,t +5378,100%,Uzbekistan,1.0,f +34211,,Uzbekistan,2.0,t +385,100%,,1.0,f +9809,100%,Malta,1.0,f +44963,0%,Togo,1.0,t +27722,,,3.0,t +17445,100%,Lebanon,1.0,f +46757,,Maldives,1.0,t +37823,100%,Gambia,3.0,f +44638,,French Guiana,1.0,t +35608,67%,,1.0,f +263,,Anguilla,1.0,f +16381,,Russian Federation,1.0,f +32704,,Nicaragua,1.0,t +38719,,,2.0,t +1262,,Estonia,1.0,t +34090,,,1.0,t +14463,,,1.0,t +43047,100%,Guinea,2.0,t +41215,,Sao Tome and Principe,1.0,f +25473,,Niue,1.0,f +2883,100%,Tanzania,2.0,f +872,100%,Tonga,1.0,f +39009,82%,Mauritania,1.0,f +17120,100%,Uzbekistan,2.0,t +18787,100%,,1.0,t +10802,,Chad,1.0,t +39156,100%,Nicaragua,1.0,f +21197,86%,Kiribati,1.0,f +34848,,,1.0,t +28768,100%,,3.0,f +24774,,,1.0,t +17172,100%,Niue,1.0,f +46471,,,1.0,t +46175,100%,United Kingdom,1.0,t +46398,,Niue,1.0,f +9584,100%,Canada,1.0,f +12268,100%,,1.0,f +46707,100%,,1.0,f +43964,,Russian Federation,1.0,f +43064,,Guinea,1.0,t +24941,,Kenya,1.0,f +26485,100%,Afghanistan,1.0,f +27022,,,1.0,t +13761,,Nicaragua,2.0,f +3362,40%,Rwanda,1.0,t +3283,,,1.0,t +35216,50%,Marshall Islands,1.0,t +19131,,Djibouti,1.0,f +2021,100%,,1.0,f +18107,,,1.0,t +6554,,Jersey,1.0,t +47776,,Brazil,1.0,f +22864,100%,,1.0,t +38535,,Vanuatu,1.0,t +38069,,Chile,1.0,t +2949,100%,French Guiana,1.0,t +47765,100%,French Polynesia,1.0,f +47317,0%,Gambia,1.0,f +19677,,Nicaragua,1.0,t +15810,100%,Niger,2.0,f +13164,33%,Mauritania,2.0,f +9281,,,2.0,t +37709,,El Salvador,1.0,f +23441,,Isle of Man,1.0,t +12377,91%,,1.0,f +14516,,Marshall Islands,1.0,f +13420,100%,Uruguay,1.0,f +42949,100%,Papua New Guinea,2.0,f +37251,100%,Bahrain,1.0,t +30661,100%,,2.0,f +34127,100%,Uzbekistan,1.0,f +47847,96%,Indonesia,3.0,f +13533,100%,,1.0,f +18236,100%,,4.0,t +46476,,Chile,1.0,t +48124,,Marshall Islands,1.0,f +47194,,,1.0,f +22719,,Estonia,1.0,f +27235,,Estonia,1.0,f +8882,,Niue,1.0,f +11322,100%,Malta,1.0,f +31514,75%,Gibraltar,2.0,f +41768,,Afghanistan,1.0,t +5262,100%,Croatia,1.0,t +21133,,,1.0,f +32651,100%,Guinea,1.0,t +12347,,Bouvet Island (Bouvetoya),1.0,f +47191,,Monaco,1.0,t +28177,100%,Kenya,1.0,f +26173,,Denmark,1.0,f +23830,,Gambia,1.0,f +23058,100%,Philippines,1.0,f +35146,,Libyan Arab Jamahiriya,1.0,t +39518,,Isle of Man,1.0,t +9389,100%,,1.0,f +47643,100%,Holy See (Vatican City State),1.0,f +48546,100%,Gambia,1.0,t +23834,,French Polynesia,1.0,t +29930,,Russian Federation,1.0,f +6314,100%,,2.0,f +6105,,,1.0,f +28274,100%,Jersey,1.0,f +36035,83%,Bahrain,1.0,f +38767,67%,Holy See (Vatican City State),1.0,f +17858,,Uganda,1.0,f +10068,100%,,2.0,t +727,,Holy See (Vatican City State),1.0,t +9162,,Isle of Man,2.0,t +34532,100%,Monaco,1.0,f +47180,,,1.0,f +685,,Sao Tome and Principe,1.0,t +3266,100%,,1.0,t +40594,,Niger,1.0,t +13711,,Brazil,1.0,t +46097,,,1.0,t +8968,67%,,1.0,f +30080,88%,Pakistan,1.0,f +10186,,Turks and Caicos Islands,1.0,f +49234,100%,Denmark,2.0,f +13450,,Kiribati,1.0,t +8595,100%,Lebanon,3.0,t +38893,100%,Tonga,1.0,f +5326,100%,,1.0,f +16074,100%,Faroe Islands,1.0,t +27643,,Kenya,1.0,f +5047,100%,Niger,1.0,f +50071,100%,Nicaragua,1.0,f +18319,,Saint Helena,2.0,f +47816,100%,,1.0,f +44600,,Guinea,1.0,t +33108,100%,Costa Rica,3.0,f +767,100%,Uganda,2.0,f +18500,100%,Lithuania,1.0,f +46948,,Kenya,1.0,f +48649,100%,Kiribati,2.0,f +33227,,,2.0,t +2786,100%,,1.0,f +16286,,,1.0,t +35305,100%,,2.0,f +11094,,Marshall Islands,1.0,t +49477,100%,,2.0,f +42528,100%,,1.0,f +6533,,Gibraltar,4.0,f +30445,,Russian Federation,1.0,f +46002,,,1.0,f +34951,100%,Uzbekistan,1.0,f +20564,,Turks and Caicos Islands,1.0,t +6115,,Estonia,1.0,f +45875,,Zimbabwe,1.0,t +35472,100%,Senegal,1.0,t +21884,,Isle of Man,1.0,t +40992,100%,,1.0,f +18937,,Russian Federation,2.0,f +3389,100%,Estonia,13.0,f +28123,100%,Gambia,1.0,f +11932,,Turkmenistan,1.0,t +16465,,Greenland,1.0,f +48602,100%,El Salvador,1.0,t +42072,,Bosnia and Herzegovina,1.0,f +43127,100%,Maldives,1.0,f +21680,100%,,1.0,t +43983,,Turkmenistan,2.0,f +11549,100%,Zimbabwe,1.0,t +25480,,,1.0,f +24659,100%,,1.0,f +39651,100%,Denmark,1.0,f +19967,100%,,1.0,f +12995,,,1.0,f +40255,,Faroe Islands,1.0,t +22289,100%,Guinea,1.0,t +40605,,,1.0,t +39907,100%,Turkmenistan,1.0,f +39884,,Reunion,1.0,t +18409,,Cape Verde,1.0,f +277,100%,,2.0,f +34808,100%,,1.0,t +48443,100%,Maldives,1.0,f +29880,100%,Tanzania,1.0,f +48814,,,1.0,f +22714,,,1.0,t +6693,,French Guiana,1.0,f +26550,,,1.0,f +3391,100%,Iran,58.0,t +17761,,Guinea,1.0,t +19246,100%,Monaco,1.0,t +39013,86%,Russian Federation,1.0,t +8122,,Monaco,2.0,t +30594,100%,,1.0,t +26813,100%,Russian Federation,1.0,t +19290,,Micronesia,1.0,t +40171,,,1.0,f +48710,80%,Libyan Arab Jamahiriya,1.0,f +38033,,,1.0,f +30199,,,1.0,f +24114,70%,Lebanon,2.0,t +3210,,,1.0,t +24251,100%,,1.0,t +40591,100%,Reunion,1.0,t +41276,,France,1.0,t +31761,,Micronesia,1.0,t +8163,,Kenya,3.0,f +44119,,Niue,1.0,t +30604,,Pakistan,1.0,f +41116,,Estonia,1.0,f +33024,,Lebanon,1.0,t +49742,,Marshall Islands,1.0,f +4822,,Lebanon,1.0,f +24722,100%,,1.0,t +10161,,Jersey,2.0,t +1662,,Estonia,1.0,t +23881,,,1.0,f +42606,100%,Micronesia,1.0,f +41927,,Marshall Islands,1.0,f +4360,100%,Guinea,1.0,f +41412,100%,Saint Helena,2.0,f +1965,94%,,1.0,f +11531,,Marshall Islands,2.0,t +40898,,San Marino,1.0,f +43313,,,1.0,f +27545,,,1.0,f +35603,,,2.0,f +21132,,Costa Rica,1.0,t +36280,,,1.0,f +32180,,Sao Tome and Principe,1.0,f +6185,,Pakistan,1.0,t +37594,100%,Russian Federation,1.0,f +37378,83%,Brazil,1.0,f +23920,,,2.0,f +35213,83%,Russian Federation,1.0,f +18468,,Malta,1.0,t +39379,100%,Guinea,1.0,f +39001,,Russian Federation,1.0,f +1391,,Congo,1.0,t +24033,100%,Uganda,45.0,f +36687,100%,Cape Verde,1.0,t +19845,,,1.0,t +47781,,Cocos (Keeling) Islands,1.0,t +24700,33%,Denmark,1.0,t +6149,100%,Zimbabwe,1.0,t +1908,100%,Sao Tome and Principe,2.0,t +25910,,Christmas Island,1.0,t +37584,100%,Puerto Rico,1.0,f +36524,100%,Saint Helena,8.0,t +37621,,,1.0,f +39247,100%,,1.0,f +13753,100%,,2.0,f +42407,100%,Maldives,2.0,t +30175,,Russian Federation,2.0,t +1864,,,1.0,f +31361,100%,,1.0,f +29917,,,1.0,t +33730,,Russian Federation,1.0,t +1804,,,1.0,f +9011,100%,Russian Federation,1.0,f +37725,100%,Zimbabwe,1.0,f +4506,70%,,1.0,f +39983,100%,,1.0,f +14,,Grenada,1.0,f +389,,,1.0,t +41864,100%,,1.0,t +1099,70%,,1.0,f +16897,,Mauritania,2.0,f +44559,100%,,1.0,t +38485,,Isle of Man,1.0,f +29228,100%,,1.0,f +35776,100%,Isle of Man,1.0,t +5357,95%,Sao Tome and Principe,1.0,f +16198,0%,Sao Tome and Principe,1.0,t +12873,,Monaco,1.0,f +47360,100%,,3.0,f +27828,,Lebanon,1.0,f +37071,,,1.0,f +19271,100%,Jersey,11.0,f +21802,,Lebanon,2.0,f +24432,100%,Andorra,2.0,t +10165,,,1.0,t +18564,,,1.0,f +13515,100%,,3.0,t +42139,,,1.0,t +37053,,Uzbekistan,1.0,f +44361,,,1.0,f +12173,100%,Uganda,9.0,f +35285,,,1.0,t +30426,100%,Kenya,1.0,f +1705,100%,Russian Federation,2.0,t +25156,,Andorra,1.0,f +41126,90%,Saint Helena,1.0,f +44274,,,3.0,f +27626,,Micronesia,4.0,f +40121,,,1.0,f +12490,,French Guiana,1.0,f +31567,50%,,1.0,f +42350,100%,Bahamas,2.0,t +45097,100%,Gambia,1.0,t +11938,,,1.0,f +38702,,Micronesia,1.0,f +45695,78%,,1.0,t +8143,,Guinea,1.0,t +2862,100%,Vanuatu,1.0,f +19843,100%,Turkmenistan,3.0,t +33439,,Russian Federation,1.0,f +35873,,,2.0,f +43176,86%,Guinea,3.0,f +33809,,,1.0,f +28540,100%,Vanuatu,12.0,f +9461,,,1.0,t +2384,,Isle of Man,1.0,f +9712,,,1.0,t +48934,,Gambia,2.0,f +27070,100%,Uzbekistan,2.0,f +36867,100%,Tanzania,1.0,f +11977,,,1.0,t +219,100%,Congo,2.0,f +33995,,Guinea,1.0,f +40673,,Marshall Islands,1.0,f +36559,,Marshall Islands,1.0,t +3949,,,1.0,f +16844,,,1.0,f +13047,,Guinea,1.0,f +44578,100%,Niue,2.0,t +12070,100%,Vanuatu,1.0,f +8929,,Indonesia,1.0,f +48774,,Jersey,1.0,f +41428,100%,,1.0,f +23345,,,1.0,t +37499,100%,French Guiana,4.0,t +9061,100%,Rwanda,2.0,t +24574,,,1.0,f +29737,100%,,1.0,f +42502,100%,Philippines,2.0,f +37406,100%,Greenland,2.0,f +26721,,United Kingdom,1.0,t +20867,100%,Guinea,4.0,t +12587,,Lebanon,1.0,t +10700,100%,Gambia,1.0,t +48737,,Russian Federation,1.0,f +49947,,Andorra,1.0,t +19239,100%,Nicaragua,2.0,t +20014,,Uzbekistan,1.0,t +36661,100%,,1.0,t +19786,0%,,1.0,t +9212,,,1.0,f +40110,,Somalia,1.0,f +32600,100%,Lebanon,1.0,f +20537,,,1.0,f +8918,100%,,1.0,t +24322,,Guernsey,1.0,t +26406,100%,Kiribati,2.0,f +39260,,,1.0,f +13983,,Micronesia,3.0,t +3906,,Anguilla,1.0,f +24367,,Niue,1.0,t +47515,0%,Kiribati,1.0,f +21407,,,1.0,t +17405,,Niue,1.0,t +28833,100%,Venezuela,5.0,f +45633,100%,Ecuador,1.0,f +787,,Faroe Islands,1.0,t +42282,,,1.0,f +36826,,,1.0,t +24639,100%,Kenya,2.0,t +16240,100%,Tanzania,2.0,t +2455,,Kenya,1.0,t +3278,90%,Guinea,2.0,t +12930,,Kiribati,1.0,f +11928,100%,,1.0,f +17103,,Somalia,1.0,f +31805,99%,Mauritania,20.0,f +22209,90%,,1.0,f +22438,90%,Guinea,2.0,t +8479,,Gambia,1.0,f +33716,100%,Gibraltar,2.0,f +18627,,Cuba,1.0,t +38387,100%,Sao Tome and Principe,1.0,t +4572,100%,,2.0,t +48654,100%,,6.0,f +37223,,,1.0,f +9147,100%,,4.0,f +38820,,Fiji,1.0,f +31830,100%,Tonga,1.0,f +29092,,,1.0,f +5101,67%,Nauru,1.0,f +43889,100%,Lebanon,2.0,f +43663,,Kenya,1.0,f +17771,90%,Russian Federation,1.0,f +49542,100%,Turkmenistan,2.0,f +47021,,,2.0,f +22865,,,1.0,f +26831,50%,Fiji,1.0,f +29419,,Lithuania,1.0,t +9399,100%,,1.0,f +14439,100%,Svalbard & Jan Mayen Islands,1.0,f +49206,100%,Sao Tome and Principe,1.0,f +42823,100%,China,1.0,f +42425,,El Salvador,1.0,f +27130,100%,Croatia,1.0,f +22739,100%,Gambia,3.0,t +32161,100%,Nicaragua,5.0,t +29804,,Malta,1.0,f +13949,,Kenya,1.0,f +4278,,,1.0,f +11600,100%,Gibraltar,1.0,t +31749,100%,Uganda,9.0,t +40455,,Guinea,1.0,f +35673,,Zimbabwe,1.0,f +4076,50%,Uganda,1.0,t +2743,97%,China,23.0,f +44347,88%,Wallis and Futuna,2.0,f +3104,43%,Marshall Islands,1.0,f +14034,100%,Russian Federation,1.0,f +10374,100%,,1.0,f +17013,100%,,1.0,f +9667,,Gibraltar,1.0,f +11778,,Monaco,1.0,t +6108,60%,Peru,1.0,f +46450,,Malta,1.0,t +17213,100%,France,1.0,t +19240,,Niue,1.0,t +28765,100%,,1.0,f +11787,,Peru,1.0,f +12041,100%,,1.0,t +46943,94%,Peru,2.0,f +13823,,Nicaragua,1.0,f +640,,Chile,1.0,f +6450,,,1.0,f +38886,,Marshall Islands,1.0,f +38683,,Rwanda,1.0,f +22324,100%,Venezuela,6.0,t +2693,,Lebanon,1.0,f +21329,,,1.0,f +15559,100%,Barbados,1.0,t +16711,,French Guiana,1.0,f +48103,,Reunion,1.0,f +16673,,France,1.0,t +4749,,Brazil,1.0,f +44722,100%,Niue,2.0,f +20089,100%,Rwanda,1.0,t +10006,100%,Tanzania,1.0,t +49136,,Denmark,1.0,t +3476,100%,Wallis and Futuna,1.0,f +23250,,,1.0,f +17214,,Russian Federation,1.0,t +12139,,Russian Federation,1.0,f +6974,100%,Barbados,1.0,t +36407,100%,Uganda,2.0,t +17198,100%,Gambia,1.0,f +12956,,,1.0,t +45960,,Peru,1.0,f +17643,,,1.0,f +40671,100%,Bosnia and Herzegovina,2.0,t +30498,,,3.0,f +4394,,,1.0,f +2112,100%,Nicaragua,2.0,f +37345,,Venezuela,1.0,t +41745,,Togo,1.0,f +21817,,Vanuatu,1.0,t +22753,,,1.0,f +34477,,El Salvador,1.0,f +77,50%,Monaco,2.0,f +40462,100%,Rwanda,1.0,f +29901,33%,Lithuania,1.0,t +48948,,,1.0,f +19791,,,1.0,t +31882,,,1.0,t +7740,,Malawi,1.0,f +329,,Bosnia and Herzegovina,2.0,f +19900,,Kiribati,1.0,f +45594,,,1.0,f +31643,,France,1.0,f +24756,,,1.0,t +11046,100%,Libyan Arab Jamahiriya,1.0,t +19040,,,1.0,f +7649,100%,Jersey,1.0,t +35505,,Peru,1.0,t +49783,,,1.0,t +30023,75%,Rwanda,1.0,t +33203,100%,,3.0,f +18903,,,2.0,t +7874,,Niue,1.0,f +32472,,,1.0,f +40766,,Russian Federation,1.0,f +22024,,,1.0,t +22234,,Chad,1.0,t +45370,,,1.0,f +7392,,,1.0,t +47372,,Micronesia,1.0,f +32417,100%,Fiji,1.0,t +17515,100%,Philippines,5.0,f +4364,100%,Uganda,1.0,f +7155,,,1.0,t +44774,90%,,1.0,t +42888,,Venezuela,1.0,f +38959,100%,,1.0,f +11071,90%,,2.0,f +9638,0%,Tonga,2.0,f +8058,90%,Niger,2.0,t +46719,,Maldives,1.0,t +12335,,Mauritania,1.0,f +2445,100%,El Salvador,1.0,t +23870,100%,Brazil,1.0,f +18060,,,1.0,t +13251,,,1.0,f +46068,,Suriname,1.0,f +13236,100%,Vanuatu,1.0,f +42915,100%,Vanuatu,1.0,f +20665,100%,,2.0,t +9224,100%,,1.0,f +29353,,Lebanon,1.0,f +7102,100%,Vanuatu,4.0,t +46493,,,1.0,f +15628,,,1.0,t +12980,100%,Uzbekistan,3.0,t +42187,,Costa Rica,1.0,t +36345,,Uganda,1.0,f +23699,100%,Lebanon,1.0,t +30826,,,1.0,f +14178,,France,1.0,t +28662,100%,Brazil,2.0,f +11168,100%,,1.0,f +8978,88%,Brazil,1.0,t +48118,100%,Guinea,1.0,t +452,100%,Ghana,3.0,f +2012,,Ecuador,1.0,f +26633,100%,Guinea,1.0,t +45771,,,1.0,f +8096,100%,Vietnam,2.0,f +16045,100%,Anguilla,1.0,t +19909,100%,Andorra,1.0,f +39986,100%,Isle of Man,2.0,t +20413,67%,El Salvador,1.0,f +43053,,Papua New Guinea,1.0,f +14934,,Anguilla,1.0,f +625,,Isle of Man,1.0,t +44626,90%,Niue,1.0,t +31871,100%,Uzbekistan,1.0,t +1841,100%,,1.0,f +22647,100%,Uzbekistan,1.0,f +15247,100%,Tunisia,1.0,f +13372,,Niue,1.0,f +45215,100%,Niue,1.0,f +43254,,Turkmenistan,1.0,t +90,,Tonga,1.0,f +10376,,Gambia,1.0,f +23686,,,1.0,t +44397,,Isle of Man,1.0,t +29968,100%,French Guiana,1.0,f +40547,,,1.0,f +2822,100%,Gambia,1.0,t +6576,100%,Gibraltar,3.0,t +26689,80%,Svalbard & Jan Mayen Islands,1.0,f +4785,100%,,1.0,f +39829,100%,,1.0,f +25914,100%,France,2.0,t +24409,100%,Isle of Man,1.0,f +40573,75%,,1.0,f +47044,,,1.0,t +16268,,,10.0,f +40629,,,1.0,f +44713,100%,French Guiana,1.0,t +28173,,Uzbekistan,1.0,f +24726,100%,,1.0,t +12758,100%,,1.0,f +32065,100%,Niue,1.0,f +32661,,,3.0,t +36543,100%,Pakistan,1.0,f +16947,,Chile,2.0,f +3303,,Nauru,2.0,f +24186,,Rwanda,1.0,t +17941,100%,France,1.0,t +8399,,Nauru,1.0,t +33431,100%,,1.0,f +48671,100%,Guinea,3.0,t +31439,,Chile,1.0,t +15848,,Ghana,1.0,f +45221,100%,Turkmenistan,1.0,t +37615,100%,Vanuatu,1.0,f +32225,,Reunion,1.0,f +24366,,Togo,1.0,f +39781,100%,Brazil,1.0,t +39161,,French Polynesia,1.0,f +38184,40%,Estonia,1.0,f +15796,,Chile,1.0,f +13721,,Maldives,1.0,f +13728,100%,Pakistan,1.0,f +40438,,,1.0,t +5014,100%,,1.0,t +33326,,Nicaragua,1.0,f +1366,25%,Fiji,4.0,f +33336,75%,Nicaragua,1.0,f +1729,75%,,2.0,f +47229,,Jersey,1.0,t +11964,,Chad,1.0,t +19250,100%,Papua New Guinea,1.0,f +13026,100%,,1.0,t +24139,,Grenada,1.0,f +23437,100%,Niger,1.0,f +41337,100%,Maldives,5.0,f +42262,,Turkmenistan,1.0,f +46394,100%,Estonia,1.0,f +15305,100%,,1.0,t +31096,,Canada,1.0,f +8131,100%,,1.0,f +24288,100%,Senegal,4.0,f +31636,,,1.0,f +17092,100%,Estonia,2.0,f +31887,,Rwanda,2.0,f +14137,,Isle of Man,1.0,f +32562,,,1.0,f +38304,100%,United Kingdom,1.0,t +14018,,Svalbard & Jan Mayen Islands,3.0,t +11474,100%,Andorra,1.0,t +32181,,,1.0,t +32631,,Chile,1.0,f +28621,100%,Marshall Islands,1.0,f +2027,,Sao Tome and Principe,1.0,f +14663,100%,Kenya,1.0,f +35891,,,1.0,f +40203,,,1.0,f +4048,100%,,1.0,f +32752,,Anguilla,1.0,f +37591,,,1.0,f +10920,100%,Kenya,2.0,t +27682,,,1.0,f +12177,100%,France,6.0,t +5508,,Barbados,1.0,f +19149,,,1.0,f +5869,,Costa Rica,1.0,f +41991,100%,Mexico,1.0,t +49496,100%,Gibraltar,1.0,f +48701,100%,Tonga,1.0,f +26558,100%,Micronesia,2.0,t +24812,100%,,1.0,t +10962,100%,Sao Tome and Principe,3.0,t +32816,88%,Denmark,1.0,f +42017,,,1.0,f +45519,100%,Puerto Rico,2.0,f +43360,100%,Denmark,2.0,f +38479,100%,Russian Federation,2.0,t +48194,,,1.0,f +4038,,Micronesia,1.0,t +36160,,Guinea,1.0,f +17398,100%,,1.0,f +7980,100%,Tonga,1.0,f +5975,100%,Guinea,2.0,f +12679,90%,,2.0,f +12242,,Chile,1.0,f +47085,100%,Sao Tome and Principe,1.0,f +49765,100%,Uzbekistan,1.0,t +13868,,,1.0,f +28057,,Cuba,1.0,f +48652,100%,Isle of Man,1.0,t +8304,,,1.0,t +23168,,Bouvet Island (Bouvetoya),3.0,t +48360,,,1.0,t +7067,100%,,1.0,f +47415,,,1.0,f +49924,67%,Vanuatu,2.0,f +38856,,,1.0,f +25716,,Ghana,2.0,f +11643,,,1.0,t +17772,,Jersey,1.0,f +20216,,United Kingdom,1.0,f +40526,90%,Kenya,1.0,f +33991,,,2.0,f +32434,,Niue,1.0,f +35768,100%,,1.0,f +42786,,Nicaragua,1.0,t +28703,100%,Uzbekistan,2.0,f +49456,0%,Venezuela,1.0,f +15539,,Venezuela,1.0,f +4920,98%,Estonia,8.0,f +6912,60%,,1.0,f +17535,100%,,1.0,t +39972,100%,Niue,1.0,f +4240,100%,,1.0,t +42532,,Lebanon,1.0,f +41546,100%,Mauritania,21.0,f +3126,,Brazil,2.0,t +15397,,Turkmenistan,1.0,t +34603,,Gambia,1.0,f +29813,100%,Holy See (Vatican City State),1.0,f +31265,90%,,1.0,t +34398,100%,,3.0,t +2014,,,1.0,f +15380,,,1.0,f +22814,,Niger,1.0,f +41429,,Malta,2.0,t +16939,,Senegal,11.0,t +36917,100%,Croatia,1.0,t +8654,100%,Christmas Island,3.0,f +27096,,Micronesia,1.0,t +3023,,,1.0,t +43815,,,1.0,t +29251,,Sao Tome and Principe,1.0,f +1782,,,2.0,f +38442,,,1.0,f +23094,,Canada,1.0,f +29203,100%,Kenya,1.0,t +591,100%,Niue,2.0,f +25605,,Micronesia,2.0,f +35743,100%,Palestinian Territory,2.0,t +46721,100%,,1.0,t +43046,,Denmark,1.0,f +27652,100%,El Salvador,5.0,f +41621,100%,,1.0,f +17040,,,1.0,f +39481,100%,Uzbekistan,1.0,t +38694,100%,,1.0,t +2688,,Lebanon,1.0,t +38112,100%,Malta,1.0,f +20211,,Holy See (Vatican City State),1.0,t +28602,100%,Mauritania,2.0,f +20198,,Nicaragua,1.0,f +25003,96%,Latvia,9.0,t +12818,,Chile,4.0,f +48810,,Uzbekistan,1.0,t +9992,,Guernsey,1.0,f +10082,100%,Isle of Man,2.0,t +37962,100%,Gibraltar,1.0,t +46087,,Sao Tome and Principe,1.0,f +7695,,Venezuela,1.0,t +458,,,1.0,t +11330,,,1.0,f +28671,100%,Malawi,2.0,f +23221,100%,Chad,1.0,f +24198,25%,French Guiana,4.0,f +35526,100%,,2.0,f +25132,100%,,2.0,f +11984,,,1.0,f +38129,,French Guiana,1.0,f +13120,83%,,1.0,f +35779,100%,,1.0,f +20758,100%,Uzbekistan,2.0,t +30583,,,1.0,f +16493,,,1.0,f +11510,,Montserrat,11.0,f +11240,,Svalbard & Jan Mayen Islands,1.0,t +47695,100%,,1.0,f +14079,,Russian Federation,1.0,f +38294,,Lebanon,1.0,f +16881,100%,Vietnam,1.0,t +8930,,Turkmenistan,2.0,t +37943,,Russian Federation,1.0,f +13397,,,1.0,f +665,100%,Greenland,2.0,t +29250,,,1.0,f +28739,,Faroe Islands,1.0,f +41098,,Chad,1.0,f +36782,,,1.0,t +8832,,,1.0,f +44833,,Venezuela,1.0,f +8518,100%,Peru,1.0,f +47326,,,1.0,f +17818,100%,Montserrat,2.0,t +35941,,,2.0,f +42739,100%,,1.0,f +22054,,,1.0,f +45977,100%,,2.0,f +5858,,Cape Verde,2.0,t +17397,,Russian Federation,1.0,f +39,,Sao Tome and Principe,1.0,f +16781,100%,,1.0,t +30846,,Nicaragua,1.0,t +12527,,,1.0,f +41474,,Turkmenistan,2.0,t +39847,100%,Venezuela,1.0,f +46133,,Lebanon,2.0,t +35391,100%,,2.0,t +13247,,El Salvador,2.0,f +11128,,Zimbabwe,1.0,t +37389,,,1.0,f +30500,100%,Zimbabwe,1.0,t +8205,,,1.0,t +31063,100%,Gambia,1.0,f +36684,,Croatia,1.0,f +14408,,Niue,1.0,t +44540,67%,,1.0,t +39632,100%,Zimbabwe,1.0,t +7395,100%,Niue,1.0,f +10949,,Bosnia and Herzegovina,1.0,f +29261,100%,Uruguay,1.0,t +24131,,,1.0,f +47555,,Lebanon,1.0,t +29931,100%,Brazil,1.0,f +25369,100%,Chad,1.0,t +38960,100%,,1.0,f +30424,,United Kingdom,1.0,t +48250,,,1.0,f +5559,,Cape Verde,1.0,t +7772,100%,,1.0,f +26981,,Philippines,4.0,f +6069,100%,,1.0,t +45721,100%,Jersey,1.0,t +3606,,,1.0,f +44783,100%,Senegal,1.0,f +6766,,Kenya,1.0,f +41265,,Zimbabwe,1.0,t +13525,100%,Nauru,1.0,f +908,,,1.0,f +28251,100%,,1.0,t +157,,,1.0,t +13931,100%,Guinea,1.0,f +15147,100%,,1.0,f +7959,100%,Peru,1.0,f +11332,100%,Croatia,2.0,t +41053,,,1.0,t +14336,,Saint Helena,1.0,f +37665,100%,Wallis and Futuna,4.0,t +38722,100%,,1.0,f +42623,,,1.0,t +3464,,Russian Federation,1.0,f +14652,,Niue,1.0,f +42771,,Russian Federation,1.0,t +10486,,Mauritania,1.0,f +29754,,Indonesia,2.0,f +31340,100%,El Salvador,1.0,t +4907,100%,Lebanon,2.0,t +16563,,Togo,1.0,t +13571,100%,,1.0,t +21529,100%,Uzbekistan,2.0,t +46664,100%,Croatia,2.0,t +39405,100%,United Kingdom,1.0,f +25602,13%,Kenya,2.0,f +18708,,Uzbekistan,1.0,f +40405,0%,Tonga,1.0,f +3254,,Tonga,16.0,t +25388,,Switzerland,1.0,t +22091,100%,Faroe Islands,9.0,f +41478,100%,Monaco,2.0,f +6892,100%,,1.0,f +11578,100%,Brazil,1.0,f +21268,0%,,2.0,f +27274,,,1.0,f +26868,100%,Mauritania,1.0,t +20238,100%,Guinea,1.0,f +11508,80%,,1.0,f +39003,,Gibraltar,1.0,t +6026,100%,Rwanda,1.0,t +10612,100%,Brazil,3.0,f +1779,,China,1.0,f +23184,100%,Lebanon,1.0,t +1285,,Kenya,2.0,f +39708,100%,Anguilla,1.0,t +4352,,United Kingdom,1.0,t +11876,,Malta,1.0,t +7707,100%,Bouvet Island (Bouvetoya),1.0,t +28790,,Gambia,2.0,f +46767,100%,El Salvador,3.0,t +38701,40%,Niger,27.0,f +43494,98%,Saint Helena,5.0,f +2422,,Gambia,2.0,t +36413,,,1.0,f +41628,100%,United Kingdom,1.0,t +9846,100%,Indonesia,3.0,t +10055,,Jersey,1.0,t +33737,,,1.0,f +49716,,Ghana,1.0,t +20381,,,1.0,t +49921,,,1.0,t +23725,,Monaco,1.0,f +18005,100%,,1.0,t +22097,100%,United Kingdom,1.0,t +47858,100%,Sao Tome and Principe,1.0,f +4620,100%,,4.0,t +19982,,Montserrat,1.0,t +34445,,Rwanda,1.0,t +14831,,Somalia,1.0,f +2284,,Turks and Caicos Islands,1.0,f +274,90%,Togo,2.0,f +13512,,Niue,1.0,t +13890,,Kiribati,13.0,f +30157,,,1.0,f +3777,100%,Kiribati,1.0,t +19243,,Zimbabwe,1.0,f +22123,100%,Holy See (Vatican City State),1.0,f +7827,,Guinea,1.0,f +47022,,,1.0,f +32920,60%,Reunion,5.0,f +29868,,,1.0,f +33171,,Gibraltar,1.0,f +29960,100%,Indonesia,1.0,t +25455,,French Guiana,1.0,f +42212,,,1.0,f +13998,,,1.0,t +5469,,Lebanon,1.0,f +9923,100%,Gambia,2.0,f +39304,75%,Togo,2.0,t +1901,100%,El Salvador,1.0,f +5023,,Pakistan,1.0,f +31218,100%,Saint Helena,1.0,f +11830,100%,Maldives,67.0,t +5290,,,1.0,f +38557,100%,,1.0,t +6460,100%,,1.0,f +46539,100%,,2.0,f +39141,,,1.0,f +23808,100%,,1.0,f +23278,,Kenya,1.0,t +15289,100%,Monaco,1.0,f +1959,,,1.0,t +50089,100%,Isle of Man,2.0,f +22083,33%,,1.0,t +24998,,Turkmenistan,1.0,t +9130,,Croatia,1.0,f +18086,,,1.0,t +4912,100%,Pakistan,2.0,f +35273,100%,Chad,1.0,t +46092,100%,Rwanda,1.0,f +13935,100%,Ecuador,2.0,f +18955,90%,Denmark,3.0,f +27253,,French Guiana,1.0,f +48169,,Uganda,1.0,t +31855,100%,Kenya,2.0,t +2170,100%,,2.0,t +41353,,Rwanda,1.0,t +15989,100%,Palestinian Territory,1.0,t +45314,,Isle of Man,1.0,t +33696,,Lebanon,2.0,t +38302,,,1.0,t +17393,,Guernsey,1.0,f +5718,,Yemen,4.0,t +33859,,France,1.0,f +34596,100%,Rwanda,1.0,f +35948,100%,Brazil,8.0,f +912,100%,Monaco,1.0,f +36731,100%,Nicaragua,7.0,f +45727,,Niue,1.0,t +37646,100%,Estonia,60.0,f +14805,,Ghana,2.0,f +15599,,Ecuador,1.0,f +9831,100%,Micronesia,1.0,f +26335,,Guernsey,1.0,t +49299,90%,Equatorial Guinea,2.0,f +24238,,,1.0,f +34013,80%,,2.0,t +33747,,Mauritania,1.0,f +10373,90%,Bouvet Island (Bouvetoya),2.0,f +29497,100%,,1.0,f +39194,,Ghana,1.0,t +24513,,Rwanda,2.0,f +14221,33%,,1.0,f +7400,100%,,1.0,f +7027,100%,Gibraltar,1.0,f +3891,,,1.0,f +21739,100%,,1.0,f +2975,,Faroe Islands,1.0,f +32315,100%,,1.0,f +32116,88%,,3.0,t +19037,,Montserrat,4.0,t +12141,,,1.0,t +20639,100%,Nicaragua,1.0,f +5735,,Turkmenistan,1.0,t +11639,0%,Russian Federation,2.0,f +26513,,Zimbabwe,1.0,f +478,100%,Holy See (Vatican City State),1.0,f +12017,100%,Guinea,1.0,f +40737,89%,Russian Federation,1.0,t +47108,,,1.0,t +3052,,Mauritania,3.0,f +2457,,,1.0,t +330,100%,United Kingdom,1.0,f +16568,,,1.0,f +43336,,France,1.0,f +19538,100%,,1.0,t +1698,,El Salvador,1.0,t +49007,,Chile,1.0,f +16079,100%,Sao Tome and Principe,1.0,f +14330,100%,Indonesia,1.0,t +39033,50%,Russian Federation,1.0,f +23714,100%,Montserrat,1.0,f +22179,,Uzbekistan,1.0,t +11544,100%,,1.0,f +9691,,Uzbekistan,1.0,t +12414,,,1.0,t +13267,,,1.0,f +48039,,,1.0,t +18061,95%,Marshall Islands,1.0,f +37606,,Reunion,2.0,f +4979,100%,United Kingdom,1.0,f +44744,,Saint Helena,1.0,f +31883,,Uganda,1.0,t +13240,100%,Estonia,1.0,t +30258,,Kenya,1.0,t +27942,,Guinea,1.0,t +42835,100%,,2.0,f +17446,,,1.0,f +49319,,Niue,1.0,f +15865,,,1.0,f +41500,100%,,1.0,t +23912,50%,,1.0,t +41216,,Jersey,1.0,t +2947,,Russian Federation,2.0,f +45689,,,1.0,f +20857,,Netherlands,1.0,t +30268,,Maldives,1.0,f +33938,100%,United Kingdom,2.0,f +34566,88%,,1.0,t +16237,100%,Sao Tome and Principe,1.0,t +49181,,,1.0,t +39801,,Holy See (Vatican City State),1.0,f +17883,100%,,1.0,f +23127,,Niue,1.0,t +47805,,Malta,1.0,f +39216,,,1.0,t +9689,100%,Guinea,2.0,f +37268,,,1.0,t +25913,89%,Uzbekistan,1.0,f +40640,,Sao Tome and Principe,1.0,f +39704,,Montserrat,1.0,t +46668,100%,Malta,2.0,t +48905,100%,,1.0,f +3944,,,1.0,t +37443,75%,,1.0,f +3945,71%,,1.0,t +48534,,Chad,1.0,f +33678,,,1.0,f +19008,100%,Kiribati,1.0,f +11776,,Holy See (Vatican City State),1.0,t +4237,33%,Holy See (Vatican City State),1.0,f +8835,,Wallis and Futuna,1.0,f +33017,,Estonia,1.0,f +3994,100%,Kenya,1.0,t +897,,Bahrain,1.0,t +36264,,,1.0,f +32763,100%,,10.0,f +34753,100%,El Salvador,1.0,t +25860,100%,Brunei Darussalam,1.0,t +10785,,,1.0,t +30868,,,1.0,f +35443,100%,French Guiana,1.0,t +8656,100%,Venezuela,1.0,f +24371,,France,1.0,f +37845,14%,,1.0,t +20368,,,1.0,t +19178,100%,,2.0,f +37383,100%,,1.0,t +24349,100%,Guinea,2.0,t +34559,100%,Isle of Man,1.0,t +5661,100%,,1.0,f +4519,,Mexico,1.0,f +49752,,Faroe Islands,2.0,f +28676,,Anguilla,1.0,f +25305,,,1.0,t +12800,100%,Malawi,2.0,f +4913,100%,Gambia,1.0,f +30906,,Isle of Man,1.0,t +22713,100%,Uganda,1.0,f +24068,,,1.0,f +43557,,Switzerland,2.0,t +1553,,,2.0,f +425,,,1.0,f +3355,78%,Faroe Islands,2.0,t +39952,100%,Tonga,21.0,f +25874,100%,Malta,1.0,t +32007,80%,El Salvador,2.0,f +49555,100%,Saint Pierre and Miquelon,4.0,t +46040,,Kenya,1.0,f +14227,80%,Svalbard & Jan Mayen Islands,1.0,t +49043,100%,Micronesia,4.0,t +21094,,Ukraine,1.0,f +40721,,El Salvador,1.0,f +49747,,,1.0,t +36852,,,1.0,f +31381,100%,Niue,2.0,f +18776,,Marshall Islands,1.0,f +26559,,Guinea,4.0,t +13747,100%,Niue,1.0,t +21249,80%,Sao Tome and Principe,1.0,f +34220,100%,Estonia,1.0,t +44593,83%,Gambia,1.0,f +21325,100%,,1.0,f +14504,,,2.0,f +8170,100%,,1.0,t +39744,,,1.0,f +44746,,Russian Federation,1.0,f +29478,90%,,1.0,t +36238,,,2.0,t +31486,,,1.0,f +44336,83%,Monaco,1.0,t +2817,,El Salvador,1.0,f +7903,,Micronesia,1.0,t +13031,,Gambia,1.0,f +43832,,,1.0,f +27597,100%,Portugal,7.0,t +18148,,Lebanon,1.0,f +3793,,Kiribati,1.0,t +34088,,Marshall Islands,1.0,f +5690,100%,Isle of Man,2.0,t +42981,100%,,1.0,f +42049,,Gambia,1.0,f +17347,100%,Jersey,1.0,f +13411,,Lebanon,2.0,t +20120,100%,Wallis and Futuna,10.0,f +17500,100%,,1.0,f +18014,,Isle of Man,1.0,f +26036,,Svalbard & Jan Mayen Islands,2.0,f +1593,,Djibouti,1.0,f +19419,,Chile,1.0,t +39242,,,1.0,t +25121,,Venezuela,1.0,t +12043,0%,,1.0,t +34873,,,1.0,f +35170,100%,Vietnam,5.0,t +43919,100%,Ghana,1.0,f +33067,,Costa Rica,1.0,f +15485,,Montserrat,1.0,t +45079,75%,Tonga,1.0,t +23455,50%,,1.0,f +33168,100%,Senegal,1.0,t +40306,,Anguilla,1.0,t +27251,100%,,1.0,f +5743,,China,2.0,t +41034,,,1.0,f +10929,,Niger,1.0,f +16294,,Libyan Arab Jamahiriya,2.0,f +35356,100%,Russian Federation,2.0,t +2776,100%,,1.0,f +37265,,Niue,2.0,t +44518,,Micronesia,1.0,f +18170,88%,France,1.0,f +29270,,China,1.0,f +7479,100%,,1.0,f +14026,,Cocos (Keeling) Islands,1.0,f +11380,100%,United Kingdom,1.0,t +4543,,Jersey,1.0,f +4011,100%,French Guiana,3.0,f +21358,100%,Congo,1.0,f +27392,90%,Sao Tome and Principe,1.0,f +22117,100%,Tonga,1.0,f +2037,80%,Gambia,1.0,f +47882,,,1.0,f +6389,,Malta,1.0,t +41540,100%,,1.0,f +36370,100%,Puerto Rico,1.0,f +31084,,,1.0,f +43115,100%,Maldives,1.0,t +313,0%,,1.0,t +21602,,Guinea,1.0,t +2989,,,1.0,f +17054,100%,Nicaragua,1.0,f +12107,100%,Fiji,2.0,t +17637,100%,Monaco,1.0,t +24506,,,1.0,f +987,,China,1.0,f +33771,,Uzbekistan,1.0,t +43207,100%,Venezuela,1.0,f +12842,,,1.0,f +4600,70%,Marshall Islands,1.0,t +19627,0%,,4.0,f +12885,,Kenya,1.0,f +9438,,,3.0,t +7949,,Guinea,1.0,f +3410,,Philippines,1.0,t +46513,,Isle of Man,1.0,f +12501,100%,Sao Tome and Principe,2.0,f +23462,,,1.0,f +6852,,Costa Rica,1.0,f +2814,,French Guiana,1.0,f +42926,,,1.0,f +17725,,,1.0,f +31763,,Niger,2.0,t +271,100%,,1.0,f +4373,,Denmark,1.0,t +42435,100%,Russian Federation,3.0,t +16472,67%,Sao Tome and Principe,1.0,t +6154,86%,Sao Tome and Principe,1.0,f +20660,,Russian Federation,1.0,t +29289,100%,Kiribati,1.0,f +48857,100%,,24.0,f +32930,100%,,1.0,f +21060,100%,Tunisia,1.0,f +11010,,Micronesia,2.0,f +18297,,,1.0,t +9124,100%,,1.0,f +36152,,Chad,1.0,f +27421,,,1.0,f +19338,,Guernsey,1.0,f +19632,,Lebanon,1.0,t +43175,100%,El Salvador,1.0,f +13975,100%,Mexico,2.0,f +39407,,,2.0,t +3514,,Marshall Islands,1.0,f +35342,,,1.0,f +45536,,Indonesia,1.0,t +3538,,,1.0,f +12765,100%,Zimbabwe,1.0,t +20796,100%,Togo,2.0,f +33174,,Pakistan,1.0,t +25044,,Faroe Islands,1.0,t +27572,67%,Niue,1.0,f +16344,100%,Nicaragua,1.0,f +19457,70%,Monaco,1.0,t +2242,90%,Monaco,1.0,f +22649,,Russian Federation,1.0,f +8083,,Guinea,1.0,f +41349,100%,Jersey,1.0,f +43731,100%,,1.0,t +13932,,,1.0,f +35921,,,1.0,t +1451,,United Kingdom,1.0,f +12611,,,1.0,f +49840,,Canada,1.0,f +48614,100%,,1.0,t +49076,97%,Kenya,12.0,t +47072,,,1.0,f +20386,,,1.0,t +18110,,Bosnia and Herzegovina,1.0,t +23434,,Niue,1.0,f +17267,,Zimbabwe,1.0,f +20909,100%,Gambia,1.0,f +15169,,,1.0,t +16615,100%,Chad,1.0,f +30368,,Uganda,1.0,f +3940,100%,Pakistan,2.0,f +43415,100%,Gambia,1.0,f +20411,100%,Pakistan,4.0,t +43795,89%,Vanuatu,1.0,f +10578,100%,,1.0,f +32579,,Nicaragua,1.0,f +27105,50%,Turkmenistan,1.0,f +6276,100%,Zimbabwe,5.0,f +38790,,,1.0,t +34207,,Estonia,1.0,t +16312,100%,Kiribati,11.0,f +21944,,Guernsey,1.0,t +2907,100%,Pitcairn Islands,2.0,t +15154,,,1.0,f +47565,100%,Kiribati,1.0,t +5432,,Maldives,1.0,f +41089,100%,Gambia,3.0,t +21644,100%,Australia,1.0,f +40157,,Ghana,1.0,t +27211,,Sao Tome and Principe,1.0,f +37301,,Togo,1.0,f +31267,,Faroe Islands,1.0,f +9948,,Ukraine,2.0,t +43048,100%,France,1.0,t +12458,,Rwanda,2.0,t +16730,,Isle of Man,1.0,f +3604,63%,Tunisia,1.0,f +37510,100%,Zimbabwe,1.0,t +25009,,Philippines,1.0,f +26562,,,1.0,f +21704,,,1.0,t +11210,29%,Estonia,1.0,f +8985,100%,Gibraltar,1.0,t +28983,,,1.0,t +44425,,Cocos (Keeling) Islands,1.0,t +25053,25%,Uzbekistan,1.0,t +27398,100%,Niue,1.0,f +14090,67%,Costa Rica,1.0,f +40678,100%,Ecuador,3.0,t +48918,100%,Slovakia (Slovak Republic),1.0,t +32715,,French Guiana,1.0,t +14740,0%,,1.0,f +32679,100%,Papua New Guinea,1.0,t +510,,Greenland,1.0,f +4587,,El Salvador,1.0,f +5037,100%,El Salvador,1.0,t +48201,,Anguilla,1.0,f +18559,100%,,1.0,t +2215,100%,,2.0,f +35712,100%,Marshall Islands,1.0,t +27015,100%,Kenya,2.0,t +17694,100%,Isle of Man,4.0,t +31609,,,1.0,f +1694,,Lebanon,1.0,f +44375,,Sao Tome and Principe,1.0,f +30843,,Niue,1.0,f +3015,100%,Nicaragua,3.0,f +11757,,Nauru,1.0,t +42805,,Russian Federation,2.0,f +40981,,,1.0,f +37237,100%,Tunisia,1.0,f +30765,,Marshall Islands,2.0,f +43134,100%,Suriname,6.0,f +11100,100%,Nicaragua,1.0,f +11946,89%,Zimbabwe,2.0,f +30732,,,2.0,f +5009,,Jersey,1.0,t +47719,,,1.0,f +11847,71%,Sao Tome and Principe,1.0,t +35813,100%,Kiribati,1.0,f +32944,,,1.0,f +49453,0%,,1.0,f +34344,100%,Denmark,1.0,f +43225,100%,,3.0,f +7696,97%,Russian Federation,7.0,t +12259,,Vanuatu,1.0,f +4125,100%,Guernsey,1.0,t +33179,100%,Rwanda,13.0,f +38968,,,1.0,f +17441,100%,,6.0,t +44712,,Nicaragua,1.0,f +27263,80%,China,5.0,f +49001,,,1.0,t +19276,,,1.0,t +9093,100%,,1.0,t +25403,,,1.0,t +5226,38%,,1.0,f +41518,100%,Montserrat,13.0,t +2683,,Guinea,1.0,t +6424,100%,,1.0,f +35348,,Suriname,2.0,t +21204,,Netherlands,1.0,t +22129,100%,Moldova,1.0,f +15887,,,1.0,f +40112,100%,Cape Verde,1.0,f +17950,,,1.0,t +21784,,Kiribati,1.0,t +1,,Anguilla,2.0,t +927,100%,,1.0,f +37314,100%,,1.0,f +4554,,Marshall Islands,1.0,f +39023,100%,Niue,1.0,t +19564,100%,Turks and Caicos Islands,2.0,t +31098,,,1.0,t +45270,100%,Jersey,1.0,t +14976,,Zimbabwe,1.0,f +44190,100%,,3.0,f +6920,100%,,1.0,f +11670,,Chile,1.0,t +36335,,,1.0,f +27516,,Papua New Guinea,1.0,t +46052,,Marshall Islands,1.0,f +3282,100%,,1.0,t +2467,100%,,1.0,t +37728,,Montserrat,1.0,f +46226,,,1.0,t +34504,,,1.0,f +27689,,Maldives,1.0,f +32410,100%,United Kingdom,5.0,f +21014,,,1.0,f +47448,100%,Gibraltar,1.0,t +31124,100%,France,3.0,f +29660,,Pakistan,1.0,f +3049,100%,,1.0,f +33873,100%,Turks and Caicos Islands,8.0,t +30212,,,1.0,f +29833,,Zimbabwe,1.0,f +13153,100%,Zimbabwe,1.0,f +42757,,Micronesia,1.0,f +43431,,French Guiana,1.0,f +18887,,,1.0,t +12034,,Bosnia and Herzegovina,1.0,f +43355,,,1.0,f +1360,86%,Guernsey,1.0,f +26449,100%,Micronesia,1.0,f +6891,,,1.0,f +32129,,Gambia,1.0,t +34079,,Chile,1.0,t +34580,,,1.0,t +29138,88%,Kiribati,2.0,f +1830,,,1.0,t +9711,89%,Micronesia,2.0,t +11212,,Nicaragua,1.0,f +16627,100%,France,1.0,f +39591,,Chad,1.0,t +22973,100%,,2.0,f +34308,,Costa Rica,1.0,t +38140,,Uzbekistan,1.0,t +49988,,Faroe Islands,1.0,t +46124,100%,Niue,1.0,f +19491,100%,Vietnam,1.0,t +36877,,,1.0,f +20110,100%,Micronesia,1.0,f +23574,,Isle of Man,1.0,t +48980,100%,Mauritania,2.0,f +18163,,,1.0,t +40039,100%,Croatia,1.0,f +47256,50%,Marshall Islands,1.0,f +10189,,,1.0,f +38537,100%,Russian Federation,1.0,f +24025,,,1.0,f +36641,100%,Vietnam,5.0,f +27483,,Djibouti,1.0,t +16631,,,1.0,t +32031,100%,Zimbabwe,1.0,t +43834,99%,Uganda,13.0,f +42879,100%,Gambia,1.0,f +17060,100%,Monaco,1.0,f +44884,,Russian Federation,3.0,t +26123,,Tunisia,5.0,t +12816,,,1.0,t +6794,100%,Bosnia and Herzegovina,2.0,t +39694,,,1.0,f +13821,100%,United Kingdom,2.0,f +18589,,Ghana,1.0,f +1718,,,1.0,f +28411,100%,,1.0,f +24441,100%,,1.0,f +19535,,Russian Federation,1.0,t +23710,100%,Gambia,5.0,f +14659,,,1.0,t +41524,,Fiji,4.0,t +30276,,Isle of Man,1.0,t +7248,100%,Nicaragua,2.0,f +25485,100%,Kiribati,1.0,f +10297,,Chad,1.0,t +16706,100%,France,1.0,t +46141,100%,,3.0,f +9880,,,1.0,f +4014,100%,United Kingdom,1.0,f +15808,,,1.0,f +29790,,Korea,1.0,t +34646,100%,Jersey,1.0,f +34296,,Kenya,3.0,f +27186,100%,Zimbabwe,7.0,t +25022,,,1.0,f +1571,,Tonga,1.0,t +11663,,Brazil,1.0,f +553,,Denmark,1.0,t +36052,100%,,2.0,t +49344,,,1.0,f +25730,,Kiribati,3.0,t +30704,,Nicaragua,1.0,f +17469,,Sao Tome and Principe,1.0,f +22059,,Uzbekistan,1.0,t +17777,,Lebanon,1.0,t +28614,50%,,1.0,f +267,,Malta,1.0,t +255,50%,Gibraltar,1.0,f +13233,,,1.0,t +48334,100%,Guinea,12.0,f +6991,,,1.0,f +48254,,Micronesia,1.0,f +38229,,,1.0,f +36607,100%,,2.0,t +21026,,,1.0,f +16564,,Russian Federation,1.0,f +7287,100%,Guinea,1.0,f +47554,,Rwanda,2.0,f +45808,100%,Uganda,1.0,f +4488,,,1.0,f +4685,43%,Fiji,1.0,f +24248,,Papua New Guinea,1.0,f +48142,,Monaco,2.0,f +3231,100%,Christmas Island,1.0,f +21047,,Denmark,1.0,f +6999,100%,Brazil,1.0,f +17873,100%,El Salvador,1.0,f +27821,50%,,1.0,f +17689,,United Kingdom,2.0,f +28278,90%,Chile,1.0,t +23758,,Anguilla,2.0,f +9068,100%,,1.0,f +25425,100%,,1.0,t +30717,,,1.0,t +11241,,,1.0,f +45057,,Marshall Islands,1.0,t +15616,83%,Tonga,1.0,f +7571,33%,Kiribati,2.0,f +28202,100%,,2.0,t +28021,83%,Isle of Man,2.0,f +25532,,Nauru,1.0,f +37836,100%,,1.0,t +41501,100%,Montserrat,2.0,t +39719,,Somalia,1.0,t +24532,,,1.0,f +8373,,Malta,1.0,t +22409,100%,,1.0,f +6817,,Gibraltar,1.0,t +33007,100%,Croatia,1.0,t +33163,100%,Marshall Islands,1.0,f +25047,,,1.0,f +44762,100%,Estonia,1.0,t +28815,,,1.0,f +22614,63%,Zimbabwe,1.0,t +30962,100%,Jersey,2.0,t +34627,100%,Tanzania,1.0,f +30606,,,1.0,f +17888,100%,,1.0,f +23668,100%,Fiji,2.0,f +41853,100%,Denmark,1.0,f +3167,100%,Micronesia,3.0,f +19522,,Ghana,2.0,t +22491,,Croatia,1.0,t +46055,,Latvia,2.0,t +22082,100%,Isle of Man,1.0,t +8367,100%,Nicaragua,1.0,f +21040,,,1.0,f +4816,90%,Somalia,3.0,f +13033,80%,Moldova,3.0,t +40026,,Niue,1.0,t +3417,,Fiji,32.0,t +40177,100%,,13.0,f +27367,100%,Maldives,1.0,f +23556,,,1.0,f +27819,100%,Kenya,1.0,f +1063,100%,Papua New Guinea,4.0,f +31966,75%,Nauru,1.0,f +38370,,Kenya,1.0,f +36171,,Sao Tome and Principe,6.0,t +44430,,,1.0,f +24592,,,1.0,f +38804,100%,,1.0,f +12272,50%,Zimbabwe,1.0,t +886,100%,Faroe Islands,1.0,f +30959,100%,Zimbabwe,2.0,f +3047,100%,Brazil,1.0,f +12066,,,1.0,f +47211,,Marshall Islands,1.0,f +40659,,,1.0,f +10469,100%,,1.0,f +35468,100%,Sao Tome and Principe,1.0,t +7839,100%,,2.0,f +23261,86%,Jersey,2.0,f +34521,,French Guiana,1.0,t +31066,,Somalia,1.0,t +24067,,Mexico,3.0,t +26706,,Anguilla,1.0,f +24115,,Uruguay,1.0,f +4541,,Chad,1.0,f +4650,100%,,3.0,f +4856,100%,Philippines,5.0,f +10108,50%,Denmark,1.0,f +3812,,Lebanon,1.0,f +5217,,Croatia,1.0,f +9746,,Gibraltar,1.0,f +25456,,,1.0,t +991,33%,Jersey,1.0,t +3820,,,1.0,f +3176,100%,Uzbekistan,1.0,t +40105,,,1.0,t +37470,100%,,1.0,t +22576,,Gambia,1.0,t +11469,100%,Guinea,2.0,f +30908,,,2.0,f +35556,,,1.0,t +40599,100%,United Kingdom,1.0,t +38569,,,1.0,f +18450,,,1.0,f +15334,,Estonia,1.0,t +2363,,Barbados,3.0,t +28884,100%,Micronesia,2.0,t +17788,0%,Australia,2.0,t +10865,88%,Isle of Man,2.0,t +44385,50%,Vanuatu,1.0,f +39087,90%,,1.0,t +22518,100%,Guinea,6.0,f +34000,,Monaco,1.0,f +13647,100%,,3.0,f +18383,,Wallis and Futuna,1.0,t +36998,92%,Lebanon,10.0,f +18695,,Kenya,1.0,f +35807,100%,Sao Tome and Principe,2.0,f +5817,,Cocos (Keeling) Islands,1.0,f +11125,100%,Nicaragua,12.0,t +37734,,Malawi,1.0,f +40593,,Nauru,10.0,t +45133,77%,Chile,1.0,f +5223,100%,Chile,1.0,t +836,,Russian Federation,1.0,t +11093,,,1.0,t +38534,100%,Maldives,1.0,f +12479,,,1.0,t +3258,,Chad,1.0,t +8736,,Niue,1.0,t +11550,,Niue,1.0,t +32046,50%,Gibraltar,1.0,t +39300,100%,Kenya,2.0,f +31878,,Lebanon,1.0,t +15761,100%,Nicaragua,1.0,f +10104,,Tonga,1.0,t +23301,,Vietnam,1.0,t +40724,,Niue,1.0,f +46999,,Russian Federation,1.0,f +37363,100%,Isle of Man,1.0,f +19667,100%,Monaco,1.0,f +8042,100%,Micronesia,1.0,t +49787,100%,,1.0,f +45089,100%,Jersey,1.0,f +22178,65%,Lebanon,1.0,t +37822,100%,,1.0,f +39372,,,1.0,f +31243,,Russian Federation,1.0,f +24345,100%,Lebanon,1.0,t +24258,100%,Malta,1.0,f +5770,100%,Zimbabwe,1.0,t +20651,,Sao Tome and Principe,2.0,f +21005,100%,Cocos (Keeling) Islands,1.0,f +16436,100%,,1.0,t +32027,,Turkmenistan,2.0,t +33982,60%,,1.0,f +16686,100%,Turkmenistan,1.0,f +39027,80%,Nicaragua,2.0,f +50025,40%,Bahrain,1.0,f +46515,100%,,1.0,f +46912,0%,France,1.0,t +6256,100%,,1.0,f +1826,,Jersey,1.0,t +42241,0%,Zimbabwe,1.0,f +31925,,,1.0,f +35508,,,1.0,t +4203,,Russian Federation,1.0,t +15144,,Croatia,2.0,t +24501,100%,Andorra,2.0,t +30425,,Micronesia,1.0,f +12961,100%,,1.0,f +44089,,Kenya,1.0,t +28174,,Denmark,1.0,f +27430,100%,Christmas Island,4.0,t +166,50%,,1.0,f +35398,100%,Isle of Man,1.0,f +7824,100%,Monaco,2.0,t +26442,33%,,1.0,f +6697,100%,Pakistan,1.0,f +29684,100%,,1.0,f +13953,100%,,1.0,f +23451,100%,Niger,3.0,f +35992,,Uganda,1.0,t +2281,,Marshall Islands,1.0,f +43323,100%,Greenland,1.0,f +22124,,Tonga,1.0,t +13338,,Gambia,1.0,f +8201,,Guinea,1.0,f +12802,100%,Niger,3.0,t +30105,,Rwanda,1.0,t +3223,100%,Kenya,1.0,t +42682,,Guinea,1.0,f +45273,,Wallis and Futuna,1.0,t +46110,100%,Afghanistan,4.0,f +29175,,Wallis and Futuna,1.0,f +48119,,Monaco,1.0,f +38278,,Bosnia and Herzegovina,1.0,t +48542,,Philippines,1.0,f +32060,,Russian Federation,1.0,t +39867,100%,Jersey,1.0,t +29052,100%,Kiribati,1.0,f +20296,,Zimbabwe,1.0,t +8046,100%,,1.0,f +2511,,,1.0,f +10479,100%,Palestinian Territory,3.0,t +21447,,Lebanon,1.0,f +5202,,,2.0,f +42098,,Maldives,1.0,t +47902,,,1.0,f +11582,100%,Anguilla,2.0,t +6894,80%,Estonia,47.0,t +36088,100%,Niue,2.0,f +23905,,Maldives,1.0,t +11373,,Ghana,1.0,f +29309,,Micronesia,1.0,t +20134,,,1.0,t +29639,100%,,1.0,t +23461,,Slovenia,1.0,f +32991,100%,Mauritania,2.0,t +26441,,Niue,1.0,f +29807,100%,Brazil,2.0,t +12671,,,1.0,f +21134,,United Kingdom,1.0,f +35156,100%,,2.0,f +38002,,,1.0,t +31430,,Niger,1.0,f +45512,100%,Barbados,2.0,t +38201,100%,Russian Federation,1.0,f +27019,100%,Gambia,1.0,f +31085,,,1.0,f +1873,100%,,2.0,t +41335,,,1.0,t +38863,100%,,1.0,t +13313,,,1.0,t +11457,90%,Denmark,1.0,t +22207,,Niue,1.0,t +31612,100%,,3.0,t +13441,100%,Togo,2.0,f +36304,100%,,1.0,t +44096,,,2.0,t +38102,,Zimbabwe,1.0,t +12006,100%,Uruguay,5.0,f +43148,100%,Slovakia (Slovak Republic),1.0,f +7482,,,1.0,t +16948,100%,Suriname,1.0,f +29447,100%,Faroe Islands,1.0,t +34836,,,1.0,t +5485,,,1.0,f +5540,100%,Rwanda,1.0,t +23384,,,1.0,t +12777,100%,,1.0,t +20192,,,1.0,t +37655,,Niue,1.0,f +45878,100%,Costa Rica,1.0,f +29198,100%,Uzbekistan,1.0,f +17672,,,1.0,f +35679,100%,,2.0,f +23856,100%,,1.0,f +45684,100%,,1.0,t +26341,,Chad,1.0,f +38232,,,3.0,t +45661,,,1.0,t +22985,100%,,1.0,f +4247,,,2.0,f +23742,,,1.0,t +7671,,Uzbekistan,1.0,f +47989,100%,,2.0,t +2778,,Anguilla,2.0,t +17905,,Papua New Guinea,1.0,f +22466,,Wallis and Futuna,1.0,f +47963,100%,,1.0,t +28957,0%,Turkmenistan,2.0,f +43136,,Chile,1.0,f +7982,100%,Serbia,3.0,f +24538,,,1.0,f +17096,,Somalia,1.0,t +20893,80%,,1.0,f +29894,,Guernsey,1.0,t +11076,100%,Malta,1.0,t +24449,,France,1.0,f +1963,100%,,1.0,t +42741,100%,Tanzania,1.0,f +31889,100%,Zimbabwe,1.0,t +14412,,,1.0,f +21345,,United Kingdom,1.0,f +21398,,Russian Federation,2.0,t +34970,,,1.0,f +14560,100%,Barbados,2.0,f +37760,100%,Niue,1.0,t +7047,,Zimbabwe,1.0,t +40045,,Marshall Islands,1.0,t +42860,100%,Guinea,1.0,f +47876,,Estonia,1.0,t +3378,,Maldives,1.0,f +14204,100%,Jersey,1.0,f +16953,,,1.0,f +32781,100%,Lebanon,2.0,f +3887,,Niue,1.0,f +937,100%,French Guiana,3.0,f +9538,,Svalbard & Jan Mayen Islands,3.0,f +48439,100%,Guinea,2.0,f +26304,,French Guiana,1.0,f +24338,100%,El Salvador,2.0,f +19099,,Lebanon,1.0,f +21898,,Russian Federation,1.0,t +27999,,Maldives,1.0,t +36584,,Denmark,1.0,t +13304,100%,Cocos (Keeling) Islands,2.0,f +9841,100%,,1.0,f +23065,,Kenya,1.0,f +44954,,Rwanda,2.0,t +9196,100%,Jersey,1.0,f +44452,100%,,2.0,t +1635,0%,Barbados,1.0,f +46541,100%,Chad,1.0,f +19010,,Philippines,2.0,f +38153,,Ecuador,1.0,t +35480,100%,Netherlands,1.0,f +34811,100%,Montserrat,1.0,f +6224,100%,Wallis and Futuna,2.0,t +27603,100%,Pakistan,1.0,t +28169,,Guernsey,1.0,f +2694,,Afghanistan,1.0,f +4002,,France,1.0,f +38339,100%,Venezuela,2.0,f +4225,100%,Costa Rica,2.0,f +23335,100%,,3.0,f +24466,,Denmark,1.0,f +25575,,Costa Rica,1.0,t +23199,,Isle of Man,1.0,f +11166,80%,Marshall Islands,1.0,f +17382,,Slovenia,1.0,t +37909,100%,Russian Federation,2.0,t +47488,0%,Gambia,1.0,f +13641,85%,Mauritania,23.0,f +27680,,,1.0,t +37906,,,1.0,t +17503,50%,,1.0,t +1164,,Tonga,1.0,t +8489,100%,Zimbabwe,2.0,f +35532,,,1.0,f +45989,90%,Isle of Man,1.0,f +21843,100%,,1.0,f +36065,,Estonia,2.0,f +49102,,,1.0,t +41962,,Afghanistan,1.0,f +23497,80%,Gambia,3.0,f +1109,,Turkmenistan,1.0,f +17959,,Lebanon,1.0,t +36963,100%,France,1.0,f +17281,,Bouvet Island (Bouvetoya),1.0,f +35773,100%,,1.0,t +35952,100%,,1.0,f +30022,33%,French Guiana,2.0,f +36344,,,1.0,t +22174,100%,Russian Federation,1.0,f +11800,,Bosnia and Herzegovina,2.0,t +6388,,French Guiana,1.0,f +49268,,Chad,1.0,t +10169,100%,,1.0,f +49160,,Estonia,1.0,f +48504,,Netherlands,5.0,f +15342,,Malawi,1.0,t +22918,,,1.0,f +18275,90%,,2.0,t +2337,100%,Kiribati,1.0,f +22913,,Tonga,1.0,t +25123,100%,Papua New Guinea,1.0,t +16073,,Chile,1.0,t +13020,,Spain,1.0,f +16108,,Monaco,1.0,f +15052,,,1.0,t +9105,,Tonga,1.0,t +22915,,Faroe Islands,1.0,t +2236,,Anguilla,1.0,f +32560,100%,Jersey,1.0,f +2633,100%,Lebanon,1.0,t +31618,100%,Maldives,1.0,f +45837,,Chile,1.0,t +48265,,Papua New Guinea,1.0,f +14218,100%,Uzbekistan,1.0,f +366,,Svalbard & Jan Mayen Islands,1.0,t +45555,,Tanzania,2.0,f +47336,,Micronesia,2.0,f +35486,100%,French Guiana,1.0,t +6500,,Reunion,2.0,f +43761,100%,,1.0,t +46089,100%,Sao Tome and Principe,3.0,t +26223,,Sao Tome and Principe,1.0,t +31845,,,1.0,t +18077,100%,Sao Tome and Principe,139.0,f +39191,,,1.0,t +44735,100%,,1.0,t +15111,,Micronesia,1.0,f +25678,100%,,1.0,f +719,100%,Estonia,1.0,f +43108,100%,Vietnam,2.0,f +4298,100%,,1.0,t +30753,100%,Anguilla,2.0,t +34572,100%,Saint Helena,1.0,f +48587,,,1.0,f +3071,100%,Indonesia,1.0,f +46322,,Denmark,1.0,f +43141,,,1.0,t +34216,,Malawi,1.0,f +18401,100%,,1.0,f +8909,100%,Tunisia,1.0,t +8314,,,1.0,f +3704,,Barbados,1.0,t +34027,100%,,1.0,f +18258,90%,Niue,2.0,f +1239,100%,Gambia,1.0,t +27423,100%,,3.0,t +9127,,,1.0,f +14211,,Niue,2.0,f +11034,90%,Chile,1.0,f +50082,,,1.0,t +27646,100%,Isle of Man,1.0,f +26895,50%,Gambia,1.0,f +19799,100%,Guinea,2.0,f +35558,,Maldives,1.0,t +94,,Micronesia,1.0,f +1018,,,1.0,f +1401,100%,Uganda,3.0,f +25530,,Senegal,1.0,t +15221,,,1.0,f +17784,,,1.0,t +30440,,Denmark,1.0,f +6454,,Bouvet Island (Bouvetoya),1.0,f +30902,100%,Senegal,1.0,f +26083,100%,Faroe Islands,2.0,t +33684,,Lebanon,1.0,f +47856,,,1.0,f +44590,100%,Faroe Islands,1.0,f +49720,,Micronesia,1.0,t +35122,100%,Guinea,1.0,t +17331,,Gambia,1.0,f +31653,,Slovenia,1.0,f +10645,100%,,2.0,t +28435,,,1.0,f +22139,,Chile,1.0,f +9167,,Rwanda,2.0,t +45154,100%,Lebanon,2.0,t +28026,,Niue,1.0,t +12100,,Bouvet Island (Bouvetoya),1.0,t +24919,100%,Estonia,4.0,f +40697,93%,,2.0,f +29410,89%,Niger,1.0,f +13310,,Niue,1.0,f +2852,100%,Isle of Man,1.0,f +1092,,Nauru,1.0,f +14367,53%,,1.0,f +19485,50%,Russian Federation,1.0,t +38716,,Estonia,1.0,f +47261,100%,Estonia,7.0,f +32742,100%,Nicaragua,3.0,t +47296,100%,,1.0,f +31888,100%,,1.0,f +47355,,,2.0,f +11806,100%,,1.0,t +36865,,,1.0,t +31695,,Nicaragua,1.0,t +29601,,Faroe Islands,6.0,t +41529,,,1.0,t +42183,100%,Puerto Rico,1.0,t +12939,,,1.0,f +23390,100%,French Guiana,1.0,f +16081,,,1.0,t +11286,,,1.0,f +25677,100%,Peru,2.0,f +42373,,,1.0,t +48747,,Marshall Islands,1.0,f +16109,100%,,1.0,f +19252,,,1.0,f +27047,100%,France,1.0,f +28125,,Reunion,2.0,t +11020,,Guernsey,1.0,t +26319,,Turks and Caicos Islands,1.0,f +6226,,Lebanon,1.0,t +4306,83%,China,3.0,f +12937,,,1.0,t +16104,50%,,1.0,f +42317,100%,Netherlands Antilles,1.0,t +13770,,Netherlands,4.0,f +23009,100%,Gambia,1.0,t +8907,100%,Costa Rica,1.0,f +17396,100%,Micronesia,1.0,f +12346,,Denmark,1.0,t +24781,100%,Svalbard & Jan Mayen Islands,1.0,t +30155,,Croatia,1.0,f +11137,80%,Niue,2.0,t +16498,100%,,1.0,f +5029,,,1.0,f +26378,,Cape Verde,5.0,f +33881,100%,Djibouti,2.0,t +45234,100%,,1.0,f +44218,100%,Kiribati,3.0,f +29748,40%,Djibouti,1.0,t +16136,67%,,1.0,f +18026,75%,,1.0,t +8140,,,1.0,f +12627,,Canada,1.0,f +24085,100%,Gibraltar,1.0,t +42505,,French Polynesia,1.0,f +11498,90%,Niue,1.0,f +3852,,,1.0,f +6410,100%,,1.0,f +36327,100%,Guinea,5.0,t +38700,,,1.0,f +44216,,United Kingdom,1.0,t +43833,,,1.0,t +32812,100%,,1.0,f +10858,100%,Maldives,1.0,t +37307,75%,Nicaragua,2.0,f +15003,,Bosnia and Herzegovina,1.0,t +23395,0%,Tonga,1.0,f +40931,,Niue,1.0,f +40037,100%,Chile,1.0,t +22677,,,1.0,f +7312,100%,,2.0,f +6736,100%,El Salvador,1.0,f +15851,100%,,2.0,f +32692,,Venezuela,1.0,f +1076,100%,Guernsey,1.0,f +15198,,Rwanda,1.0,t +33934,100%,Wallis and Futuna,1.0,f +39799,50%,Russian Federation,6.0,f +1077,,,1.0,f +40341,100%,,1.0,t +17667,,Uganda,2.0,f +45785,,,1.0,f +23576,100%,,3.0,f +42866,,,1.0,t +7252,100%,Nicaragua,1.0,f +28487,,,1.0,f +37778,92%,Tonga,24.0,f +43074,100%,,1.0,f +40321,100%,,1.0,f +21781,100%,Isle of Man,1.0,t +33815,,Isle of Man,1.0,t +4458,,French Guiana,1.0,t +33837,,Nicaragua,2.0,t +17798,100%,,1.0,t +6083,,,1.0,t +47541,100%,Palestinian Territory,1.0,t +35218,100%,Turkmenistan,2.0,f +4159,,Niue,1.0,f +21758,100%,Guinea,1.0,f +27100,,Kenya,1.0,t +14629,,Bosnia and Herzegovina,2.0,t +36623,,,1.0,f +40262,,Barbados,1.0,f +29896,,Tonga,1.0,t +35352,,Estonia,2.0,t +28364,,Guernsey,1.0,t +20412,100%,Jersey,1.0,t +1184,100%,Bahrain,2.0,f +336,100%,Ecuador,7.0,f +2128,100%,United Kingdom,1.0,f +3941,100%,,3.0,f +34359,,Indonesia,1.0,f +31954,,Niue,1.0,f +4265,100%,Palestinian Territory,2.0,t +40222,,,1.0,f +48388,,Gibraltar,1.0,f +10254,,Russian Federation,1.0,f +34558,100%,Philippines,1.0,f +30369,100%,Libyan Arab Jamahiriya,1.0,t +16791,,France,1.0,f +18713,,,1.0,t +453,,Kenya,1.0,f +48886,100%,Netherlands,4.0,t +7557,100%,Uganda,1.0,f +28061,100%,Denmark,5.0,f +29979,90%,,1.0,t +45211,100%,Micronesia,2.0,t +4230,,Isle of Man,1.0,t +4672,,,1.0,f +49905,,United Kingdom,2.0,t +29364,,,1.0,t +6137,,Guinea,1.0,f +39928,100%,,1.0,f +14949,,,1.0,f +24112,100%,Russian Federation,1.0,t +6888,100%,Sao Tome and Principe,1.0,t +20807,100%,Mauritania,2.0,t +31301,,China,1.0,f +8397,100%,Faroe Islands,1.0,t +21212,75%,,1.0,f +49035,98%,Maldives,6.0,f +28371,,Bouvet Island (Bouvetoya),1.0,f +29157,100%,Canada,1.0,t +2189,,Maldives,1.0,t +22761,,,1.0,f +33275,83%,Netherlands,1.0,f +12850,,France,1.0,f +15188,,,1.0,f +37652,100%,Kiribati,3.0,t +30532,67%,Russian Federation,1.0,f +26789,,Guinea,3.0,t +26244,,,1.0,f +16515,100%,Indonesia,1.0,f +16716,,,1.0,f +8666,50%,,2.0,t +2160,100%,Isle of Man,1.0,t +22027,67%,Togo,1.0,f +33468,100%,Monaco,2.0,t +32998,100%,,1.0,f +41346,97%,Estonia,7.0,t +34786,,,1.0,f +28312,100%,Lebanon,2.0,t +19234,100%,,1.0,f +10715,90%,,1.0,t +42635,100%,Nicaragua,1.0,f +20138,100%,,1.0,t +7627,,Zimbabwe,1.0,f +28288,100%,,1.0,f +45400,100%,,2.0,t +38473,,Micronesia,1.0,f +48242,,Anguilla,1.0,f +17409,,Niue,1.0,f +4759,,Barbados,1.0,f +36369,93%,,1.0,f +24945,100%,Lebanon,1.0,t +3805,,Uzbekistan,1.0,f +6583,,Pakistan,1.0,t +44769,0%,Fiji,1.0,f +20703,100%,,1.0,f +6011,100%,Cuba,1.0,f +19798,,,1.0,f +28799,,Brazil,1.0,t +32100,,Chad,1.0,t +21135,,Afghanistan,1.0,t +10689,100%,Isle of Man,1.0,t +46486,100%,Nicaragua,1.0,f +25253,,Nicaragua,1.0,f +12220,,El Salvador,1.0,t +41533,,Bouvet Island (Bouvetoya),1.0,f +7104,,,1.0,f +7226,,Croatia,2.0,t +2116,,Jersey,1.0,t +15259,,Nicaragua,2.0,t +25137,,Guinea,1.0,t +41794,,Puerto Rico,1.0,t +10540,100%,Togo,1.0,f +32227,,,1.0,f +5837,60%,Sao Tome and Principe,2.0,f +29735,86%,Isle of Man,2.0,f +9275,100%,Papua New Guinea,1.0,f +13940,100%,,1.0,f +28539,100%,,1.0,f +41756,100%,Chad,1.0,f +923,,Jersey,1.0,t +1160,100%,France,1.0,t +4060,100%,Russian Federation,1.0,f +1638,100%,Jersey,1.0,f +13828,,France,1.0,t +36243,,,1.0,f +10683,100%,Tonga,1.0,f +4565,100%,Turkmenistan,1.0,f +15156,80%,El Salvador,1.0,f +43698,,,1.0,f +6556,,Senegal,1.0,f +29118,,Chile,1.0,f +45854,,Niger,1.0,f +34409,100%,,1.0,t +47445,,Peru,1.0,f +34973,,Chile,1.0,f +27507,100%,Lebanon,2.0,f +20933,100%,,1.0,t +10594,88%,,2.0,t +36803,100%,Gibraltar,2.0,t +35319,,Micronesia,1.0,f +9988,,Niue,1.0,t +22050,100%,Peru,2.0,f +14043,100%,Chad,1.0,f +41415,,Lebanon,1.0,f +28370,0%,,1.0,f +34103,,Chile,1.0,t +49896,,,2.0,f +18630,,Costa Rica,1.0,f +2720,,Isle of Man,1.0,f +13924,,China,1.0,f +45630,,Cook Islands,1.0,t +26621,25%,,1.0,t +1529,100%,Denmark,1.0,f +32903,100%,,2.0,f +27071,100%,El Salvador,1.0,f +13459,,Malawi,1.0,f +35354,100%,Micronesia,1.0,f +4493,100%,,1.0,f +45669,,Somalia,1.0,f +24603,100%,Senegal,1.0,f +11848,100%,,1.0,f +14968,,Uganda,1.0,t +12424,100%,Pakistan,2.0,t +16959,,Marshall Islands,1.0,f +31853,,Montserrat,1.0,f +47053,,Guinea,1.0,t +10838,78%,Ecuador,2.0,t +46374,,Monaco,2.0,t +16028,100%,Niue,1.0,f +7510,100%,,1.0,t +20959,,,1.0,f +15070,,French Guiana,1.0,t +700,,Guinea,2.0,t +10110,100%,Costa Rica,1.0,t +47114,100%,,2.0,f +44599,100%,,2.0,t +31533,100%,Malawi,1.0,f +39467,100%,Nicaragua,4.0,t +33055,,Isle of Man,1.0,t +30296,100%,,1.0,f +15431,,Chad,1.0,t +35177,,Kenya,1.0,t +26741,,Philippines,1.0,t +648,,Zimbabwe,1.0,f +20338,,United Kingdom,1.0,t +37641,,Gambia,1.0,f +8740,,Guinea,1.0,t +35958,,Tonga,1.0,t +5254,100%,Malawi,1.0,f +47837,,Croatia,1.0,t +46031,,,1.0,t +49770,100%,Zimbabwe,1.0,f +47077,100%,Bouvet Island (Bouvetoya),2.0,f +40548,100%,,1.0,t +40434,50%,Kiribati,1.0,f +32441,,Christmas Island,1.0,f +29744,100%,Micronesia,1.0,f +34676,100%,,4.0,t +4003,0%,,1.0,t +1381,,Anguilla,1.0,t +30509,,Gambia,1.0,f +47913,0%,Monaco,1.0,f +33924,,Uganda,1.0,f +34746,100%,,2.0,f +26074,100%,,1.0,t +4478,100%,,2.0,f +1964,100%,Netherlands,1.0,f +10366,,United Kingdom,1.0,t +6418,,,1.0,t +22418,100%,Senegal,1.0,t +15385,100%,Cook Islands,3.0,f +20772,,Turks and Caicos Islands,1.0,f +32942,,Pakistan,6.0,f +37548,100%,Russian Federation,2.0,f +16757,,,1.0,t +21638,90%,,3.0,t +3194,100%,,1.0,t +46006,,Mauritania,2.0,f +32275,,Uganda,1.0,t +27710,,Costa Rica,1.0,t +15227,100%,Reunion,1.0,f +8126,56%,,2.0,f +1088,,Turkmenistan,1.0,f +14747,100%,Jersey,1.0,t +1610,100%,Niue,1.0,f +39587,,,1.0,f +42378,,Nauru,1.0,f +19133,,Rwanda,2.0,f +38218,100%,Malawi,1.0,t +13565,,Brazil,1.0,t +27851,,United Kingdom,6.0,t +8591,100%,,1.0,f +10746,,,1.0,f +16771,,Maldives,1.0,t +46554,100%,,1.0,f +2053,,,1.0,t +41006,,,1.0,t +39640,100%,Russian Federation,1.0,t +28378,,Senegal,1.0,f +5085,,,1.0,f +30083,0%,Barbados,1.0,t +16186,,Cuba,1.0,f +17734,100%,Jersey,1.0,t +22939,100%,Cocos (Keeling) Islands,1.0,f +36471,,,1.0,f +48174,,Denmark,1.0,f +9556,100%,,1.0,f +11576,100%,Maldives,1.0,f +25117,,Gibraltar,1.0,f +28913,,Jersey,1.0,t +19758,100%,Marshall Islands,1.0,f +29062,100%,,1.0,f +40065,,Guinea,1.0,f +41741,100%,Brazil,9.0,t +12551,100%,Niger,3.0,f +34688,100%,Gambia,2.0,t +7354,100%,Cape Verde,1.0,f +13229,100%,Sao Tome and Principe,1.0,t +42039,71%,,1.0,t +120,100%,Cuba,2.0,f +33077,,,1.0,t +20763,,Marshall Islands,2.0,t +45718,100%,,2.0,t +9670,,Zimbabwe,1.0,f +4528,,Bosnia and Herzegovina,1.0,t +31736,,Chile,1.0,t +687,100%,Kiribati,2.0,t +39435,100%,,1.0,t +9284,100%,Nicaragua,4.0,f +1916,,Tonga,1.0,f +41156,,,1.0,t +34036,,Turks and Caicos Islands,1.0,t +2525,50%,Montserrat,2.0,f +25692,,Lebanon,1.0,t +27503,,Lebanon,1.0,t +3525,,,1.0,f +21583,,,1.0,f +1641,,Lebanon,1.0,t +17738,,,1.0,f +23887,,,1.0,t +4874,100%,,1.0,t +48719,,,1.0,t +38525,100%,,2.0,f +44141,90%,Zimbabwe,1.0,f +5516,100%,Saint Helena,1.0,f +47323,50%,Turks and Caicos Islands,7.0,f +7816,100%,,1.0,f +1936,,Lithuania,1.0,f +47881,,Tonga,1.0,t +16793,,Vanuatu,1.0,f +47243,,,1.0,f +42463,,Lebanon,1.0,f +14357,,,2.0,t +12039,88%,,1.0,f +43578,90%,Kiribati,7.0,f +20240,,Uzbekistan,1.0,f +32228,,,1.0,t +4446,100%,Uganda,58.0,f +14718,,,1.0,f +26056,,Kiribati,1.0,f +16180,,Russian Federation,1.0,t +21210,64%,,1.0,f +23859,,Jersey,1.0,t +30283,100%,Brazil,15.0,f +327,100%,Tonga,2.0,t +35935,,Tunisia,1.0,t +9323,,Marshall Islands,1.0,f +32192,100%,,1.0,t +27017,100%,Senegal,2.0,t +9116,,Lebanon,1.0,f +4080,,,1.0,f +40847,,,1.0,f +28101,67%,Gibraltar,1.0,f +9828,0%,Puerto Rico,1.0,f +31443,,,1.0,f +39144,,,1.0,f +10113,,Gambia,1.0,f +43057,100%,Niue,1.0,t +49644,100%,,1.0,t +9810,100%,,2.0,t +2416,100%,Guinea,5.0,f +21445,100%,,1.0,t +42327,80%,,1.0,f +20262,,Marshall Islands,1.0,f +35339,,Canada,1.0,f +17698,,,1.0,t +40024,,,1.0,f +31936,100%,Vanuatu,2.0,f +5285,,Uganda,1.0,f +24316,,Niue,1.0,t +33933,100%,Papua New Guinea,3.0,f +9998,,,1.0,t +12485,,,1.0,f +20443,100%,Uganda,1.0,f +15615,,Chad,1.0,t +17598,,Uzbekistan,1.0,t +1110,,Cape Verde,1.0,t +48826,,Svalbard & Jan Mayen Islands,1.0,t +24852,100%,Wallis and Futuna,3.0,f +7593,100%,Senegal,2.0,f +6967,100%,Russian Federation,1.0,f +18518,100%,El Salvador,1.0,f +23855,94%,France,2.0,t +27993,,Maldives,1.0,t +44333,,,1.0,f +9659,,El Salvador,1.0,f +23109,,Faroe Islands,1.0,t +43220,100%,Korea,1.0,t +33619,100%,Sao Tome and Principe,1.0,f +983,100%,Marshall Islands,1.0,f +33166,100%,Cocos (Keeling) Islands,1.0,t +23818,,Congo,1.0,f +38622,,,1.0,f +21174,100%,,1.0,f +1519,,Turkmenistan,3.0,t +27752,100%,Zimbabwe,2.0,f +24324,100%,Niger,2.0,f +47356,,Denmark,1.0,f +24509,,,1.0,f +3870,33%,,1.0,f +29588,100%,,3.0,f +9666,33%,,1.0,t +33563,,Zimbabwe,1.0,t +27650,100%,Slovenia,1.0,t +32252,,Sao Tome and Principe,1.0,t +47741,,Uzbekistan,1.0,f +5281,,Anguilla,1.0,t +16345,,Micronesia,1.0,f +29981,,,1.0,f +11409,,Russian Federation,1.0,f +3708,,Bosnia and Herzegovina,1.0,t +32811,,,1.0,t +8296,,Malawi,1.0,f +21747,67%,Gambia,1.0,t +3969,0%,,2.0,f +33621,,Lebanon,1.0,t +1883,90%,Reunion,2.0,t +7145,,Niue,2.0,t +38469,100%,Cocos (Keeling) Islands,3.0,t +23904,,Honduras,2.0,t +26907,,,1.0,f +35117,100%,Venezuela,1.0,t +23238,100%,Reunion,2.0,t +14310,,Faroe Islands,1.0,f +40746,100%,Slovakia (Slovak Republic),2.0,f +23431,100%,Indonesia,1.0,t +17826,,,1.0,f +2961,,,1.0,t +44020,100%,,2.0,t +1608,100%,Zimbabwe,2.0,f +18385,100%,Brazil,2.0,t +26482,,Estonia,1.0,f +30035,100%,,1.0,f +31767,,Russian Federation,1.0,f +12040,,China,1.0,t +5723,100%,,1.0,f +25384,100%,Slovakia (Slovak Republic),1.0,f +33262,90%,Niger,1.0,f +22606,,Chile,2.0,t +35934,,,1.0,f +46301,100%,Gambia,1.0,f +1686,,Zimbabwe,1.0,f +44741,100%,,1.0,t +27134,,Guinea,1.0,f +8377,,,1.0,f +1895,,Turks and Caicos Islands,1.0,f +13365,100%,Indonesia,7.0,t +33092,,Andorra,1.0,t +41439,,Monaco,1.0,f +22089,,,2.0,f +27042,,,1.0,f +8225,95%,Barbados,31.0,f +4903,,Anguilla,1.0,t +38680,100%,Tonga,1.0,f +44941,100%,,1.0,f +2673,93%,Anguilla,2.0,f +44346,80%,Niue,1.0,t +24197,100%,Ghana,1.0,f +12101,100%,,2.0,f +18551,,Faroe Islands,1.0,f +49696,100%,Maldives,2.0,t +30471,100%,Russian Federation,1.0,t +15178,,United Kingdom,1.0,f +4386,,Zimbabwe,4.0,t +47517,,Nauru,1.0,f +18178,,Micronesia,1.0,f +1220,,Zimbabwe,2.0,f +14603,100%,,1.0,f +18306,90%,Jersey,5.0,f +25959,,,2.0,t +15025,,Niue,1.0,t +38765,,Micronesia,1.0,f +31446,100%,Faroe Islands,9.0,f +29027,,Costa Rica,1.0,f +36512,0%,,1.0,f +43266,0%,Russian Federation,1.0,f +49435,100%,Marshall Islands,6.0,t +7513,,,1.0,t +35722,,,1.0,f +5417,,Lebanon,1.0,t +14338,,Zimbabwe,1.0,f +18610,,,1.0,f +42009,,Tonga,1.0,t +3006,100%,,1.0,f +3333,,,1.0,t +13729,,,2.0,f +5561,100%,,2.0,f +7820,100%,,1.0,f +7701,100%,,4.0,f +32029,100%,Saint Helena,2.0,t +22531,,Cape Verde,1.0,f +25853,,Malawi,1.0,f +25618,,Indonesia,1.0,t +35093,100%,,1.0,f +27260,,,1.0,f +23734,0%,Uganda,1.0,t +49286,,Guernsey,1.0,t +9655,100%,,2.0,f +47370,,Marshall Islands,3.0,t +1013,100%,Venezuela,4.0,f +30289,,,1.0,t +4330,94%,Ukraine,60.0,t +20543,,Marshall Islands,1.0,f +6925,67%,Uganda,1.0,t +41140,90%,Nicaragua,2.0,t +17988,100%,Jersey,1.0,f +25034,100%,Nicaragua,2.0,f +38110,100%,,1.0,f +15986,,,1.0,t +8499,100%,Mauritania,1.0,f +44374,,,1.0,t +41534,,,1.0,f +11156,100%,El Salvador,2.0,f +31358,,,1.0,f +34526,,Brazil,1.0,t +6102,100%,Uzbekistan,1.0,f +15572,,Cape Verde,2.0,t +33399,,Micronesia,1.0,t +36693,90%,,3.0,f +46731,86%,,1.0,f +16840,100%,Mexico,1.0,t +43712,,Sao Tome and Principe,1.0,f +27271,,Mexico,1.0,f +14327,100%,Gambia,2.0,t +27790,,,1.0,f +29781,,Turkmenistan,2.0,t +19272,,,1.0,f +15963,100%,Svalbard & Jan Mayen Islands,1.0,f +4576,100%,French Guiana,1.0,t +49014,100%,Tonga,1.0,t +46233,,Bosnia and Herzegovina,2.0,f +19411,,Nauru,1.0,f +25637,100%,French Guiana,3.0,t +20357,100%,,3.0,t +43615,100%,,1.0,f +5173,100%,El Salvador,2.0,f +26033,,,1.0,t +27678,,,1.0,t +29425,,,1.0,f +9773,,Malta,1.0,f +45482,,,1.0,f +21320,,Malta,1.0,t +38732,,Chad,2.0,t +46410,100%,,1.0,f +20299,,Uganda,1.0,f +22274,,,1.0,f +30553,,United Kingdom,1.0,f +29602,100%,China,1.0,f +4108,,Chile,2.0,t +42050,100%,Turks and Caicos Islands,1.0,f +47154,,,1.0,f +46470,100%,,1.0,t +18088,100%,Marshall Islands,3.0,t +33564,100%,Maldives,1.0,t +18532,100%,,1.0,f +24215,,China,1.0,f +30118,100%,Nicaragua,1.0,f +22321,100%,United Kingdom,16.0,f +23516,,Uzbekistan,1.0,t +29058,,Uzbekistan,1.0,t +22674,100%,,3.0,f +33523,100%,Venezuela,2.0,f +39138,,Ghana,1.0,f +36433,100%,,1.0,f +43137,,Svalbard & Jan Mayen Islands,1.0,t +42279,,Tonga,1.0,f +21940,,Turkmenistan,1.0,f +8402,,,1.0,f +6592,100%,Togo,1.0,f +31662,,Malta,1.0,f +7223,,,4.0,f +25105,100%,Uzbekistan,2.0,t +5263,92%,Sao Tome and Principe,1.0,t +47974,100%,Lebanon,2.0,f +23661,100%,,2.0,t +5771,98%,Vietnam,8.0,f +34864,,,1.0,t +12357,100%,Gambia,4.0,f +421,,Turkmenistan,1.0,f +2147,,,2.0,t +28283,90%,Ecuador,1.0,t +39243,,,1.0,f +3144,0%,Puerto Rico,1.0,f +750,,Gambia,2.0,t +21605,83%,Nicaragua,6.0,f +13396,100%,Uruguay,1.0,f +34550,100%,Maldives,1.0,t +23326,100%,,1.0,t +27302,,Micronesia,1.0,t +10138,,,1.0,f +5701,100%,Costa Rica,1.0,t +8674,,,1.0,f +33254,,Uganda,1.0,f +35540,88%,,2.0,f +15199,,Kiribati,1.0,t +22377,100%,Russian Federation,1.0,t +38260,,Russian Federation,1.0,t +22376,100%,Vanuatu,6.0,f +19501,,Uzbekistan,1.0,t +25079,100%,,1.0,f +15391,90%,Niger,2.0,t +37438,100%,Andorra,4.0,t +36703,,Monaco,1.0,t +8392,80%,,1.0,f +27592,100%,,1.0,f +34114,,Saint Helena,1.0,t +12210,100%,United Kingdom,2.0,f +47268,,Kiribati,1.0,t +12123,80%,Puerto Rico,8.0,t +35767,,Faroe Islands,1.0,t +22626,,,1.0,f +40190,,,1.0,f +20164,,,1.0,t +43812,,,1.0,f +8983,100%,Anguilla,7.0,f +23340,,,1.0,f +13691,,Russian Federation,1.0,f +16719,100%,Denmark,1.0,f +9968,100%,Indonesia,1.0,f +4628,,,1.0,t +3551,,Gambia,1.0,f +37092,,Pakistan,1.0,f +44145,100%,Rwanda,4.0,t +18860,100%,,4.0,t +25939,100%,Nauru,2.0,f +24191,,Malta,1.0,f +19144,100%,San Marino,1.0,f +26361,100%,Vanuatu,3.0,f +11792,100%,,3.0,f +35787,100%,Pakistan,1.0,t +31363,100%,Faroe Islands,1.0,t +45470,,Estonia,1.0,f +32059,,,1.0,f +2187,,Togo,1.0,f +23620,100%,,1.0,f +1617,100%,Russian Federation,2.0,f +49359,100%,Isle of Man,1.0,f +43010,,,2.0,f +26107,100%,Niue,3.0,f +15050,,Switzerland,1.0,f +15661,33%,Russian Federation,4.0,f +25944,100%,,1.0,t +48354,,,1.0,f +24458,,Rwanda,1.0,f +7952,,Andorra,1.0,f +41732,,Isle of Man,1.0,t +602,,Montserrat,1.0,f +8770,100%,El Salvador,3.0,f +42200,,Chile,1.0,t +18315,100%,Bosnia and Herzegovina,1.0,f +16456,100%,,1.0,t +48768,,,2.0,t +35206,100%,Marshall Islands,1.0,f +1637,,Uzbekistan,1.0,f +13054,,Uganda,1.0,t +22792,,,1.0,t +32122,100%,,1.0,t +37457,,Turkmenistan,1.0,f +19905,97%,Monaco,8.0,f +36822,100%,,3.0,f +28682,,Chile,1.0,f +43560,,Turks and Caicos Islands,1.0,f +5908,,,2.0,t +34032,,,1.0,f +2878,93%,Bosnia and Herzegovina,3.0,t +6142,100%,,1.0,t +8267,100%,Vietnam,1.0,f +46262,100%,Kenya,1.0,t +38413,,Bosnia and Herzegovina,1.0,f +14684,,,1.0,f +48420,,,1.0,t +4732,80%,Micronesia,2.0,t +27848,,,1.0,f +43529,100%,,3.0,t +2659,100%,,3.0,t +48405,,France,1.0,f +49528,0%,,1.0,f +8440,,,1.0,f +96,100%,Mexico,2.0,t +39423,100%,,6.0,t +7484,100%,Pakistan,1.0,t +5548,,,1.0,t +41579,100%,,1.0,f +30394,100%,Marshall Islands,1.0,f +6134,,,3.0,t +6376,,,1.0,f +5371,,,1.0,t +40454,,Zimbabwe,2.0,f +11900,,Uzbekistan,1.0,f +13057,100%,Ukraine,1.0,f +35933,75%,Isle of Man,1.0,t +44906,100%,,1.0,f +20469,33%,Guinea,1.0,t +5423,,Guernsey,1.0,f +47192,,Russian Federation,1.0,t +36366,,Lebanon,2.0,t +24448,100%,Venezuela,1.0,f +27495,,,1.0,f +4154,,French Guiana,2.0,t +11432,,,1.0,f +11676,,Slovakia (Slovak Republic),1.0,f +47099,,,1.0,t +36936,100%,,1.0,t +16709,100%,Isle of Man,1.0,f +30981,0%,,1.0,f +23624,,Holy See (Vatican City State),1.0,f +19933,,Guernsey,1.0,f +1652,88%,Greenland,2.0,f +27001,,Guernsey,1.0,f +30844,,Gibraltar,1.0,f +22731,100%,Nicaragua,1.0,f +33403,,Niue,1.0,f +15053,25%,Uzbekistan,1.0,t +25091,,Isle of Man,1.0,t +1822,100%,Anguilla,1.0,f +25046,100%,Maldives,14.0,f +11973,100%,Niue,7.0,t +37076,,Bouvet Island (Bouvetoya),1.0,f +42270,,Uzbekistan,1.0,f +30773,,Bahrain,1.0,t +5479,100%,,3.0,f +4127,,,1.0,f +49386,,,1.0,f +49629,,Isle of Man,1.0,f +48455,89%,Uzbekistan,20.0,t +7339,100%,Zimbabwe,1.0,f +35841,100%,Saint Helena,4.0,f +20605,,China,1.0,t +2243,100%,Tonga,1.0,f +44703,86%,,1.0,t +27981,100%,Cape Verde,1.0,f +38360,,Mauritania,2.0,f +42840,100%,Niger,1.0,f +36890,90%,Uzbekistan,1.0,t +26792,,Bouvet Island (Bouvetoya),1.0,t +11923,,Bouvet Island (Bouvetoya),1.0,f +13103,100%,Costa Rica,1.0,f +43428,73%,Faroe Islands,9.0,t +31110,,,1.0,f +20769,,,, +7051,0%,Malta,1.0,f +47340,,Maldives,1.0,t +47117,100%,Niue,1.0,t +20803,,,1.0,f +2816,,Togo,1.0,t +30905,100%,,1.0,f +17483,90%,Vanuatu,1.0,f +43583,,Zimbabwe,1.0,f +12488,,China,2.0,f +2707,,Montserrat,1.0,f +43294,100%,Isle of Man,1.0,f +29926,67%,Zimbabwe,7.0,t +33005,,,1.0,f +1726,93%,Isle of Man,9.0,t +29073,90%,Canada,1.0,t +23035,100%,,2.0,f +42119,100%,Djibouti,2.0,t +33434,,Gambia,1.0,f +12262,100%,Niue,2.0,f +6812,100%,Uzbekistan,1.0,t +14359,,,1.0,t +44169,97%,Portugal,39.0,f +42544,100%,,1.0,f +39635,,,1.0,t +36298,0%,Malta,1.0,t +34922,100%,Canada,1.0,f +33557,90%,Netherlands,2.0,t +14798,,Jersey,1.0,t +48340,,Turkmenistan,1.0,t +34621,,Svalbard & Jan Mayen Islands,1.0,t +16648,100%,France,1.0,t +41155,,,7.0,f +17259,100%,Guinea,2.0,f +48994,100%,Rwanda,1.0,f +35875,,,1.0,f +34126,100%,French Guiana,1.0,t +1042,,,1.0,t +29322,100%,Gambia,1.0,f +11174,,,1.0,f +24615,,,1.0,t +15359,,,2.0,t +45898,100%,Andorra,1.0,t +30341,,Ecuador,1.0,f +23071,,Venezuela,1.0,t +37054,,Sao Tome and Principe,1.0,t +18390,100%,Malta,1.0,t +11748,,,1.0,f +41816,50%,Micronesia,1.0,t +12441,,,1.0,f +8036,,,1.0,t +26048,,Mauritania,1.0,f +16455,,,1.0,t +11687,100%,Tanzania,2.0,f +27215,100%,Reunion,1.0,t +4534,,Malawi,1.0,t +12074,,Mauritania,1.0,f +7917,100%,,1.0,f +31782,,Costa Rica,2.0,f +30065,,Russian Federation,1.0,t +39128,,French Guiana,1.0,f +46561,100%,,1.0,t +39615,,Niue,2.0,f +47036,,Russian Federation,1.0,t +30111,,,1.0,f +41001,,,1.0,f +14766,100%,Rwanda,2.0,t +17612,100%,Peru,2.0,t +17348,,,1.0,f +49130,100%,Niue,1.0,t +22625,100%,Nicaragua,3.0,t +16541,0%,,1.0,f +34134,25%,,1.0,f +5587,100%,,1.0,f +3699,,Monaco,1.0,t +5642,100%,El Salvador,1.0,t +21277,100%,Isle of Man,1.0,t +18974,,,1.0,f +38401,100%,Guinea,1.0,f +40021,100%,,1.0,f +19166,100%,Brazil,2.0,t +12089,100%,Zimbabwe,1.0,t +46162,100%,Nicaragua,1.0,f +33418,,,1.0,f +48616,100%,El Salvador,2.0,t +7133,90%,,3.0,f +20265,100%,Malawi,3.0,f +35471,,Russian Federation,1.0,t +3626,,,1.0,f +41860,,Niue,1.0,f +10122,100%,,1.0,t +45961,100%,,1.0,f +35977,100%,Marshall Islands,1.0,f +44309,,Gambia,1.0,t +43935,100%,Puerto Rico,1.0,f +34959,,Maldives,2.0,t +33365,,Russian Federation,2.0,f +6655,100%,Niue,1.0,f +20445,,,1.0,f +21836,100%,Mauritania,3.0,f +11349,0%,Venezuela,1.0,f +13144,,,1.0,t +34723,100%,Peru,12.0,f +45997,0%,United Kingdom,1.0,f +34823,,Russian Federation,1.0,t +37770,100%,Anguilla,2.0,t +46427,81%,Sao Tome and Principe,4.0,t +29208,100%,Libyan Arab Jamahiriya,1.0,f +48277,100%,Rwanda,4.0,f +22861,,Tanzania,1.0,f +12274,100%,Lebanon,1.0,f +37234,100%,Gambia,1.0,f +37248,,Senegal,2.0,t +10446,100%,Gibraltar,1.0,f +6438,,Vanuatu,2.0,f +10766,,Lebanon,1.0,f +5084,100%,Congo,1.0,f +35680,,Bouvet Island (Bouvetoya),1.0,t +25386,,,1.0,f +34158,,Kenya,1.0,f +25764,100%,,1.0,t +23707,90%,,1.0,f +30201,,Jersey,1.0,t +5607,100%,Jersey,2.0,t +31828,100%,Isle of Man,2.0,t +20069,100%,Monaco,3.0,f +27692,,Micronesia,1.0,f +42091,,Zimbabwe,1.0,f +39915,100%,Vanuatu,1.0,f +41266,100%,Papua New Guinea,1.0,f +26528,,,1.0,f +41971,100%,Rwanda,1.0,t +41450,33%,,1.0,f +33002,,Cuba,1.0,f +20784,100%,,2.0,t +672,100%,,1.0,t +41659,100%,,1.0,t +49262,,Isle of Man,1.0,t +27241,,,1.0,t +19286,,Tonga,1.0,t +6603,100%,Gibraltar,1.0,f +3918,,,1.0,f +34987,,Russian Federation,2.0,t +17769,,Estonia,1.0,f +32340,,Anguilla,1.0,f +24340,,,1.0,t +40027,,Cape Verde,1.0,f +7956,100%,Malta,1.0,f +9709,,Tunisia,1.0,f +48351,,Malta,3.0,f +11404,,Sao Tome and Principe,1.0,f +36350,100%,Niger,1.0,t +8281,100%,,1.0,f +5184,,Mauritania,1.0,t +3860,100%,Uzbekistan,1.0,t +34304,33%,Micronesia,1.0,f +14473,100%,Anguilla,1.0,f +3934,100%,Rwanda,4.0,f +31531,,Isle of Man,1.0,f +47778,100%,Cocos (Keeling) Islands,1.0,f +35313,100%,,2.0,f +47164,100%,Ukraine,2.0,f +16831,,Ghana,1.0,f +46607,100%,Brazil,4.0,f +27960,69%,Suriname,6.0,f +18926,100%,,1.0,t +44194,100%,Indonesia,5.0,t +6293,,Sao Tome and Principe,1.0,f +43542,67%,Russian Federation,2.0,f +12928,100%,,2.0,t +45758,100%,Brazil,1.0,f +46456,0%,Monaco,1.0,f +27021,100%,Denmark,1.0,f +39713,100%,Denmark,4.0,f +22308,100%,Kiribati,1.0,f +25296,100%,Monaco,1.0,f +27717,,,1.0,t +40807,,,1.0,f +25905,100%,Denmark,1.0,f +7932,,,2.0,f +21038,100%,Zimbabwe,1.0,f +19205,,Chad,1.0,t +28306,,Ecuador,1.0,t +22732,,France,1.0,f +35228,100%,,1.0,f +49362,100%,Niue,1.0,f +42361,100%,Sao Tome and Principe,1.0,t +16211,,,1.0,f +45392,,Netherlands,1.0,t +2076,100%,New Zealand,2.0,f +25504,,Papua New Guinea,1.0,t +49091,100%,Guinea,1.0,t +2960,,Niue,1.0,t +32209,,Lebanon,1.0,f +6371,100%,,1.0,f +15820,100%,,3.0,f +43930,100%,Kenya,1.0,t +13866,,,1.0,f +16356,,,1.0,t +14413,100%,,1.0,f +45054,70%,Zimbabwe,1.0,t +22632,100%,Togo,1.0,f +48511,90%,Christmas Island,1.0,t +32493,,Russian Federation,1.0,f +30031,100%,Turks and Caicos Islands,2.0,t +5215,,Cocos (Keeling) Islands,1.0,f +27613,,Fiji,1.0,t +13089,100%,Russian Federation,1.0,t +34584,,Faroe Islands,2.0,f +42561,,,1.0,f +29209,,Vietnam,1.0,f +46857,100%,Marshall Islands,1.0,t +33015,,Marshall Islands,1.0,t +45982,100%,,1.0,f +25151,100%,Marshall Islands,1.0,t +24855,,Bosnia and Herzegovina,2.0,t +16539,,United Kingdom,1.0,f +23362,100%,Monaco,1.0,t +14250,,Marshall Islands,3.0,t +8574,100%,Niue,2.0,f +19749,100%,Denmark,6.0,t +40587,100%,Niue,2.0,t +11475,100%,Papua New Guinea,4.0,f +14195,100%,,2.0,f +36419,,Russian Federation,2.0,f +23959,,Tunisia,1.0,t +32158,,Faroe Islands,1.0,f +29369,100%,Tonga,2.0,f +40687,,Russian Federation,1.0,f +43205,,Bouvet Island (Bouvetoya),2.0,t +50016,,Guinea,1.0,f +21873,,,1.0,f +42804,100%,Chad,1.0,f +28731,100%,Faroe Islands,1.0,t +32214,0%,Indonesia,1.0,f +26418,50%,Niue,1.0,t +18732,,France,1.0,f +37375,,Gambia,1.0,f +41808,,Brazil,1.0,t +47535,,Lebanon,1.0,t +10130,100%,Maldives,1.0,f +8232,,Tunisia,1.0,t +4848,,Croatia,1.0,f +41381,100%,Niue,1.0,f +6006,,Monaco,2.0,t +41110,,Kenya,1.0,t +32611,100%,Ecuador,1.0,f +9477,,Monaco,1.0,t +36215,100%,Rwanda,1.0,f +24930,50%,,1.0,f +23612,100%,Tonga,1.0,f +25507,100%,Malta,3.0,f +37936,,,1.0,f +44983,50%,Lebanon,1.0,t +49739,100%,,1.0,f +45399,,United Kingdom,1.0,f +6392,,Nicaragua,1.0,t +32834,100%,Faroe Islands,2.0,f +27755,100%,France,1.0,f +40706,100%,,2.0,f +1530,,Niue,1.0,f +11994,,,1.0,t +4524,100%,Vietnam,1.0,f +11169,,Tonga,3.0,t +8671,0%,Costa Rica,1.0,f +16164,,,2.0,t +41724,,Bouvet Island (Bouvetoya),2.0,f +17345,,Maldives,1.0,t +13831,100%,Russian Federation,1.0,t +6261,,,1.0,f +30658,100%,United Kingdom,2.0,f +38550,,Chad,1.0,f +22001,,,1.0,t +35081,,Canada,1.0,f +40092,,Rwanda,1.0,f +31913,100%,China,1.0,t +30318,,,1.0,t +20142,,Russian Federation,1.0,f +7255,100%,Bouvet Island (Bouvetoya),8.0,t +38808,100%,Cape Verde,1.0,f +10363,,Rwanda,1.0,t +17180,0%,,1.0,f +44031,,Guernsey,1.0,t +13660,,Reunion,1.0,f +36814,,Peru,2.0,f +32567,,France,1.0,f +26688,67%,Jersey,1.0,t +22887,100%,,2.0,f +38769,100%,Russian Federation,1.0,t +48130,,,1.0,f +36593,100%,,1.0,f +1899,,,1.0,t +44057,,Jersey,1.0,t +24626,100%,Cocos (Keeling) Islands,3.0,f +38001,,,1.0,f +22248,,Malta,1.0,t +21352,,,1.0,f +15657,,Zimbabwe,1.0,f +19991,,China,1.0,f +42133,,Tunisia,1.0,t +37231,100%,Russian Federation,1.0,f +9185,89%,Saint Helena,1.0,f +46535,50%,,1.0,f +46258,100%,Philippines,1.0,t +22600,83%,Malta,2.0,f +4208,,Guernsey,1.0,t +47066,100%,Denmark,1.0,t +43527,,France,1.0,f +30634,,Uzbekistan,1.0,t +49269,,,1.0,t +22741,,United Kingdom,1.0,f +13884,,,2.0,f +11160,100%,Gibraltar,1.0,f +20041,100%,Uzbekistan,2.0,t +48683,100%,Uzbekistan,2.0,t +10226,,Tonga,1.0,f +18566,90%,Nicaragua,2.0,t +39703,,,1.0,f +19395,,Isle of Man,1.0,f +26174,,,1.0,f +6613,100%,,1.0,f +11251,100%,Libyan Arab Jamahiriya,2.0,f +48769,100%,Indonesia,4.0,t +18016,100%,Somalia,1.0,t +2013,,Faroe Islands,1.0,t +33744,,,1.0,f +13877,,Fiji,1.0,t +7631,50%,Croatia,1.0,f +5910,,Croatia,1.0,f +17975,80%,,1.0,f +17910,,Reunion,1.0,t +16149,,,1.0,t +25447,,Rwanda,1.0,f +18405,,Nauru,1.0,t +22916,,Isle of Man,1.0,f +48500,100%,Canada,1.0,t +45170,100%,Lebanon,1.0,t +29985,,Bouvet Island (Bouvetoya),1.0,f +48839,,Australia,1.0,t +4424,,Malawi,1.0,t +2263,100%,Guinea,1.0,f +7222,100%,,2.0,f +19904,100%,Kiribati,1.0,t +25660,,Monaco,1.0,f +5194,,Sao Tome and Principe,2.0,f +7981,100%,Sao Tome and Principe,4.0,t +4641,100%,,1.0,f +20354,100%,Kiribati,1.0,f +13762,77%,Senegal,1.0,t +41696,,,1.0,f +36116,,Lebanon,2.0,f +12129,,,1.0,f +43481,100%,Russian Federation,4.0,t +38273,,Malta,1.0,t +33664,100%,French Guiana,1.0,f +27426,0%,,1.0,f +29571,,,2.0,f +9452,,,1.0,f +12859,0%,,1.0,f +4764,,,1.0,f +7581,100%,Niue,1.0,f +34219,20%,Zimbabwe,3.0,f +27351,,Gambia,1.0,t +27725,100%,Lithuania,1.0,f +24278,,Sao Tome and Principe,1.0,f +19351,100%,Guinea,2.0,f +43946,,,1.0,f +26137,,Estonia,2.0,f +41843,,Lebanon,1.0,f +1082,,Denmark,1.0,f +47342,,Nicaragua,1.0,f +26537,100%,Turks and Caicos Islands,2.0,f +2178,100%,Jersey,2.0,t +16772,,Niue,2.0,t +21222,,Isle of Man,1.0,f +21838,100%,Guinea,1.0,t +43537,50%,Costa Rica,1.0,t +24988,,,1.0,t +5942,,Russian Federation,1.0,t +14200,,Maldives,1.0,t +33335,100%,Gibraltar,3.0,t +20143,,Senegal,1.0,t +38047,90%,,2.0,f +16452,80%,Afghanistan,1.0,t +48200,57%,French Guiana,1.0,t +13766,,France,1.0,f +39421,,,1.0,f +26914,50%,Anguilla,1.0,f +30960,,Turkmenistan,1.0,f +37788,,,1.0,f +16816,,Faroe Islands,1.0,t +26373,100%,Lithuania,1.0,f +36973,100%,,1.0,t +11811,,Denmark,4.0,f +34730,100%,,2.0,f +28991,,Estonia,1.0,f +16898,,Kiribati,1.0,f +26690,,,1.0,f +30751,,,1.0,f +1998,,Nicaragua,1.0,t +48869,100%,Slovakia (Slovak Republic),22.0,f +17076,,Bahrain,1.0,t +9104,100%,,1.0,f +20373,,Chile,1.0,f +6980,,Costa Rica,1.0,t +3000,100%,,1.0,f +8494,,,2.0,f +21713,,Svalbard & Jan Mayen Islands,1.0,f +24863,,,1.0,f +34108,,French Guiana,1.0,t +19267,90%,,2.0,t +13167,100%,Micronesia,1.0,f +43301,100%,Tunisia,1.0,f +31125,,Denmark,1.0,f +42047,,,1.0,f +3475,,Marshall Islands,1.0,f +10827,,,1.0,f +29035,,Nicaragua,2.0,f +9175,,Lebanon,1.0,t +45321,100%,Brazil,2.0,f +21051,0%,Denmark,1.0,t +17656,100%,Monaco,1.0,f +43642,90%,Denmark,6.0,f +31546,,,1.0,f +14382,89%,Switzerland,1.0,f +47539,100%,Tunisia,1.0,f +8811,,Montserrat,3.0,t +41754,100%,Zimbabwe,1.0,t +25244,,Sao Tome and Principe,1.0,t +48381,90%,Netherlands,1.0,f +10089,,Libyan Arab Jamahiriya,1.0,t +44293,,,1.0,f +4959,,Brazil,1.0,f +25428,,Cook Islands,2.0,f +2710,40%,,1.0,f +13510,100%,Fiji,3.0,t +47497,100%,Micronesia,1.0,f +19838,,Chad,1.0,f +4272,,Greenland,1.0,f +36448,,,1.0,f +28050,100%,Malta,1.0,f +23879,,Isle of Man,1.0,f +11927,,Guinea,1.0,f +15086,75%,Kenya,2.0,f +22961,100%,Niger,1.0,f +26277,,Kiribati,1.0,f +28311,100%,,1.0,t +34714,100%,,2.0,t +35999,100%,Mauritania,3.0,t +33314,,Croatia,1.0,f +24568,100%,Brazil,1.0,f +41218,100%,,1.0,t +20534,,Brunei Darussalam,1.0,f +12999,100%,Micronesia,1.0,f +43785,,Russian Federation,1.0,t +15011,,Lebanon,1.0,t +47187,100%,Gambia,1.0,t +24040,,Rwanda,1.0,f +24816,,,1.0,t +43953,,,2.0,f +33740,100%,,1.0,f +34975,,Netherlands,1.0,f +11294,,Gambia,1.0,t +48841,,,1.0,f +38236,,Finland,2.0,f +7710,100%,French Guiana,1.0,t +11506,,,1.0,f +40799,100%,Togo,2.0,f +16923,,,1.0,f +44933,,Niue,1.0,f +12994,80%,Croatia,1.0,f +20165,94%,Ukraine,30.0,f +5601,,,1.0,t +21467,,Denmark,1.0,f +3811,,Micronesia,1.0,t +7493,100%,Monaco,1.0,t +48843,100%,,1.0,f +44787,,,1.0,f +36694,,,1.0,t +27377,,Marshall Islands,1.0,f +34425,100%,Isle of Man,1.0,f +39765,100%,United Kingdom,2.0,t +16715,,,1.0,t +9307,100%,Puerto Rico,1.0,f +6174,100%,Chad,1.0,t +27586,,Tonga,1.0,t +12658,,,1.0,f +39535,100%,,1.0,f +1502,100%,Uzbekistan,1.0,f +27234,,Niue,1.0,f +47186,100%,Faroe Islands,2.0,f +20074,,,1.0,f +15943,100%,Puerto Rico,2.0,f +15832,0%,Denmark,1.0,f +1095,,,1.0,f +1308,,Sao Tome and Principe,1.0,t +23552,,Turks and Caicos Islands,1.0,f +48785,100%,,2.0,t +682,,Togo,1.0,f +42377,100%,Marshall Islands,2.0,f +7914,,Chile,1.0,f +40683,100%,United Kingdom,2.0,t +15536,100%,,1.0,f +22266,,Monaco,1.0,t +17210,100%,Maldives,2.0,f +44462,100%,Russian Federation,1.0,f +17533,0%,Mauritania,1.0,f +26920,,,1.0,t +3173,,Wallis and Futuna,1.0,t +38341,,,2.0,t +10370,100%,Russian Federation,2.0,f +3483,100%,Micronesia,4.0,t +41791,100%,Tunisia,1.0,t +48332,100%,,1.0,f +39195,90%,Brazil,2.0,t +30930,,,1.0,f +46388,,Kenya,1.0,f +37225,,,1.0,f +26815,,,1.0,t +14334,90%,Tunisia,1.0,t +41105,100%,,1.0,f +15882,,Maldives,1.0,t +20642,100%,,1.0,f +1245,,Kiribati,1.0,t +23092,,Isle of Man,1.0,t +35705,,,1.0,f +14072,70%,Micronesia,1.0,f +40750,90%,Brazil,1.0,f +4621,,,1.0,t +36706,100%,Mexico,1.0,f +39676,86%,Guinea,2.0,f +45515,,,1.0,f +5834,100%,Marshall Islands,2.0,f +32,,Guinea,1.0,f +16549,100%,Costa Rica,1.0,f +38697,,France,1.0,f +41901,,,1.0,f +6301,,Malta,1.0,t +46942,,Holy See (Vatican City State),1.0,t +40366,97%,Micronesia,14.0,t +40741,,Saint Helena,1.0,t +19120,100%,Peru,1.0,t +25264,,Kenya,1.0,t +46685,100%,Tonga,1.0,t +8129,100%,Rwanda,5.0,f +44761,90%,Niger,1.0,f +20683,,Faroe Islands,1.0,t +40603,,,1.0,t +33769,100%,,1.0,t +31865,,,1.0,f +49373,,Russian Federation,2.0,t +17413,100%,Isle of Man,2.0,t +21642,100%,Denmark,1.0,f +34675,100%,Svalbard & Jan Mayen Islands,1.0,t +350,,Niue,1.0,f +49751,100%,,2.0,f +11913,100%,Gambia,5.0,f +21420,,Lithuania,1.0,f +33150,100%,Malta,1.0,f +13612,50%,,1.0,f +6857,,,1.0,f +14880,,Nicaragua,1.0,f +48708,100%,,1.0,t +19761,,Reunion,1.0,f +39157,67%,,1.0,t +21782,,Bouvet Island (Bouvetoya),1.0,t +26712,,,1.0,t +12516,,,1.0,t +39696,100%,,1.0,f +25488,100%,Gambia,1.0,t +17754,100%,Mauritania,1.0,f +8379,,,1.0,t +11827,90%,Venezuela,2.0,t +4396,100%,,1.0,f +6465,,Costa Rica,1.0,f +18977,,Nauru,1.0,t +14449,,,1.0,f +29240,67%,Kenya,1.0,f +16176,100%,Christmas Island,1.0,f +2573,,,1.0,f +4383,,Micronesia,1.0,f +1665,,Denmark,1.0,f +39724,86%,Sao Tome and Principe,2.0,f +22487,100%,Kenya,1.0,f +5846,,,1.0,f +12035,100%,Uzbekistan,1.0,t +21627,,Niue,1.0,f +12474,,,1.0,f +37933,,Sao Tome and Principe,1.0,f +7475,,Lebanon,1.0,f +22749,,,1.0,f +15090,,,1.0,t +3845,,Bosnia and Herzegovina,1.0,f +20958,100%,,1.0,f +43925,,,1.0,t +15101,56%,,1.0,f +11038,,Lebanon,2.0,t +25896,,,1.0,f +49414,100%,Tanzania,1.0,f +21308,,Bosnia and Herzegovina,1.0,f +15340,100%,Slovakia (Slovak Republic),1.0,f +15888,30%,,2.0,t +50048,,Papua New Guinea,1.0,f +4290,,Sao Tome and Principe,1.0,f +39764,,Venezuela,2.0,t +46018,,Russian Federation,1.0,f +328,,Estonia,2.0,f +2627,,Afghanistan,1.0,f +2478,,Mauritania,1.0,f +26348,,Malta,1.0,f +20897,0%,Mauritania,1.0,t +23227,,Isle of Man,1.0,t +6228,75%,Libyan Arab Jamahiriya,1.0,f +33866,100%,,1.0,t +2787,,Uzbekistan,1.0,f +39284,,,1.0,t +37215,100%,Indonesia,4.0,t +8391,,,1.0,f +33392,,Estonia,1.0,t +8172,,Bouvet Island (Bouvetoya),1.0,f +40695,,Niue,1.0,t +47536,,,1.0,f +47811,100%,Vietnam,2.0,f +12705,,,1.0,t +34476,,Lebanon,1.0,f +49961,100%,Russian Federation,3.0,t +13845,,Ghana,1.0,t +37554,100%,Nauru,1.0,t +24783,,Chad,1.0,t +33406,100%,Reunion,4.0,f +15097,,,1.0,t +42845,,Estonia,2.0,t +39684,,Svalbard & Jan Mayen Islands,2.0,f +5229,,Niue,1.0,f +17321,,Sao Tome and Principe,1.0,f +23449,100%,,1.0,t +45349,,Isle of Man,1.0,t +16468,,Greenland,1.0,f +16921,100%,Malta,1.0,f +14233,,Malawi,3.0,t +42944,,Wallis and Futuna,1.0,f +15707,100%,Vietnam,4.0,f +26112,50%,Russian Federation,1.0,t +44022,,,1.0,f +10303,90%,Turkmenistan,1.0,f +1354,,Christmas Island,1.0,f +26068,,,1.0,t +9003,,Bouvet Island (Bouvetoya),1.0,t +19269,,Bahrain,1.0,f +36401,,,1.0,t +16928,,Marshall Islands,1.0,f +9778,,Isle of Man,1.0,f +30635,,Russian Federation,1.0,t +22154,,Lebanon,1.0,f +20557,75%,,1.0,t +48100,,Indonesia,1.0,t +42588,,El Salvador,1.0,t +16281,100%,Kiribati,1.0,f +4515,100%,Gambia,2.0,f +48000,100%,Afghanistan,1.0,f +15369,100%,,2.0,t +38815,,Croatia,2.0,t +28737,,,1.0,f +15381,,Monaco,1.0,t +15859,100%,Zimbabwe,1.0,f +26128,,,1.0,f +42580,100%,Costa Rica,3.0,f +580,,Costa Rica,1.0,f +12164,100%,Nicaragua,8.0,f +40665,,Niue,1.0,f +48247,,Turks and Caicos Islands,1.0,f +42188,,Niger,1.0,f +15571,100%,,1.0,f +43920,,Christmas Island,1.0,f +19097,100%,Russian Federation,2.0,f +34253,,Lebanon,1.0,f +27169,,,1.0,f +6859,,Philippines,2.0,t +25329,,Kiribati,2.0,t +1499,100%,Tonga,2.0,t +43008,98%,Costa Rica,5.0,f +24490,,Svalbard & Jan Mayen Islands,1.0,f +29707,67%,Gambia,1.0,f +18049,,Moldova,1.0,t +30707,,Guinea,1.0,f +24400,,Tonga,1.0,f +9272,,Lebanon,1.0,f +46613,,Zimbabwe,2.0,f +40486,,Sao Tome and Principe,2.0,t +34082,100%,,1.0,f +7333,100%,Russian Federation,1.0,f +26627,71%,Monaco,1.0,f +16419,,French Guiana,1.0,t +47850,,Russian Federation,1.0,f +14748,80%,Nicaragua,1.0,f +14151,,Croatia,1.0,f +2115,100%,Jersey,2.0,t +46740,,Mauritania,2.0,f +24769,100%,Isle of Man,1.0,t +32499,100%,Niue,1.0,f +22316,100%,Zimbabwe,1.0,t +30334,,,1.0,f +13583,100%,,1.0,t +46109,83%,,1.0,f +3242,,,1.0,f +43690,,,1.0,f +18368,100%,Gambia,2.0,f +7426,,Marshall Islands,1.0,f +10334,100%,Micronesia,1.0,t +8174,,Jersey,2.0,t +14470,100%,Nicaragua,1.0,f +30090,,Libyan Arab Jamahiriya,1.0,f +36850,,,1.0,f +38014,,United Kingdom,1.0,f +46164,,Niger,1.0,f +19790,50%,Niue,1.0,f +49329,,,1.0,f +171,,,1.0,f +49719,86%,,1.0,t +14821,,Afghanistan,1.0,t +23460,,Monaco,1.0,f +13861,,,1.0,f +41755,100%,Isle of Man,2.0,f +6632,100%,Micronesia,3.0,f +13027,60%,Saint Helena,1.0,f +17029,,Kenya,1.0,t +41418,100%,Nicaragua,1.0,f +18694,100%,Chad,1.0,t +40855,,,1.0,f +14318,95%,China,2.0,f +6564,,Uganda,9.0,f +35881,,Kenya,1.0,f +24026,98%,Cape Verde,69.0,t +32362,100%,Marshall Islands,4.0,t +6907,100%,Somalia,1.0,f +16521,100%,Cocos (Keeling) Islands,1.0,t +13032,,Niger,1.0,f +9801,100%,Niger,1.0,f +26812,100%,United Kingdom,1.0,t +18419,100%,,7.0,t +34906,100%,Uzbekistan,1.0,t +49485,,Malta,1.0,f +29800,,,1.0,t +16662,,,1.0,t +36214,100%,,1.0,f +1450,,,1.0,t +12972,,Tonga,1.0,f +4220,,El Salvador,1.0,t +26100,,Jersey,1.0,f +1877,100%,Guinea,1.0,f +646,,Estonia,1.0,f +29171,100%,Mauritania,1.0,f +1296,,,1.0,f +15080,100%,Venezuela,3.0,f +31649,40%,Mauritania,1.0,t +8931,,,1.0,t +12379,100%,Estonia,1.0,f +27297,,Isle of Man,1.0,t +7576,,United Kingdom,1.0,t +40164,100%,Kenya,1.0,f +47286,100%,Micronesia,2.0,f +12183,,Turkmenistan,1.0,f +1562,,,1.0,f +19763,90%,,1.0,t +32014,100%,,2.0,f +21910,,Chad,2.0,f +34431,,Russian Federation,1.0,t +34203,100%,Uzbekistan,2.0,t +43576,100%,Guinea,2.0,t +3924,,Malta,2.0,f +11646,100%,Gambia,2.0,f +48806,100%,Togo,1.0,f +22355,100%,Malta,1.0,f +9395,,Guernsey,4.0,f +26869,,,1.0,f +35573,,Greenland,1.0,t +4801,,Gibraltar,1.0,t +11069,100%,Micronesia,1.0,f +42384,,,2.0,t +5506,,Ghana,1.0,f +4820,100%,Lithuania,2.0,t +46343,90%,Niue,2.0,f +48533,100%,Somalia,1.0,t +25769,100%,Uzbekistan,1.0,f +44046,100%,Cuba,1.0,t +9448,,,1.0,t +35675,,,1.0,t +44475,100%,El Salvador,1.0,f +40757,100%,United Kingdom,1.0,t +13505,,,1.0,f +4008,94%,,1.0,f +13679,100%,,1.0,f +19454,,,1.0,f +6364,,Russian Federation,1.0,f +44025,,Tonga,1.0,t +2352,,,1.0,t +23603,100%,,1.0,f +4555,,Mauritania,1.0,t +48896,100%,Isle of Man,1.0,f +12202,,Marshall Islands,2.0,f +7488,,,1.0,f +22720,92%,Nicaragua,1.0,f +40435,,Croatia,1.0,t +16347,67%,,1.0,t +20949,,,1.0,t +42005,,Faroe Islands,1.0,f +11449,100%,,1.0,t +8800,100%,,3.0,f +33358,,,1.0,t +11780,,Niue,1.0,f +31748,100%,Lithuania,1.0,t +5823,100%,Tanzania,1.0,t +18230,,El Salvador,1.0,f +30526,100%,Canada,3.0,t +14676,,Niue,1.0,f +33200,,China,1.0,f +47746,,Niue,1.0,f +21960,,Isle of Man,1.0,t +29674,,,1.0,f +41882,,,1.0,f +19943,63%,Isle of Man,34.0,f +34957,,Guinea,1.0,f +42698,,Niger,1.0,f +11715,,Costa Rica,5.0,f +42885,,,1.0,f +19890,,Somalia,1.0,f +30838,,,1.0,f +32009,100%,Maldives,1.0,t +18042,100%,Uganda,2.0,t +42487,,Russian Federation,1.0,t +11699,,El Salvador,1.0,t +4558,30%,,3.0,f +31907,100%,Tonga,1.0,t +15697,80%,Finland,1.0,t +29828,,,1.0,f +35970,100%,,1.0,f +20072,,Jersey,1.0,f +11658,67%,,2.0,f +3950,100%,,1.0,t +1304,100%,Montserrat,2.0,f +22718,100%,,1.0,t +45430,,Kenya,2.0,t +41949,100%,Brazil,2.0,t +35968,,,2.0,f +31718,100%,France,6.0,f +32297,,United Kingdom,1.0,f +29226,100%,Russian Federation,1.0,f +12397,100%,Niue,1.0,f +34335,100%,Jersey,2.0,t +1058,,Brazil,1.0,t +15544,,Lebanon,1.0,t +1006,100%,,1.0,t +3712,100%,Zimbabwe,1.0,f +22180,,Lebanon,1.0,t +19314,,,1.0,f +16790,100%,Vanuatu,1.0,f +4124,,,2.0,f +35032,90%,Afghanistan,1.0,t +43305,,Isle of Man,1.0,t +39990,100%,,1.0,f +28924,100%,,1.0,f +40443,,Gambia,1.0,t +2699,,Niger,1.0,f +20476,,,1.0,f +38677,,Malta,1.0,t +28483,,Lebanon,1.0,f +3661,,Anguilla,1.0,f +36312,100%,Bouvet Island (Bouvetoya),1.0,f +32999,,French Guiana,1.0,t +46396,,Marshall Islands,1.0,f +15751,100%,Tanzania,1.0,f +17410,,,1.0,f +27220,,,1.0,t +42135,,Turks and Caicos Islands,1.0,t +47857,,French Guiana,1.0,t +36513,,Tonga,1.0,t +30403,,Zimbabwe,1.0,t +38724,,Isle of Man,1.0,f +22919,100%,,2.0,t +47091,90%,United Kingdom,3.0,f +20060,,Guernsey,5.0,t +42132,100%,Marshall Islands,1.0,f +38182,100%,Kiribati,4.0,f +23063,,Zimbabwe,1.0,t +35198,,Venezuela,1.0,f +13212,100%,Zimbabwe,12.0,t +1920,67%,Sao Tome and Principe,1.0,f +38050,,,1.0,f +14537,,,1.0,f +21818,100%,Malta,1.0,f +26207,100%,,2.0,t +22644,,Mexico,1.0,f +21039,100%,Vanuatu,1.0,f +13771,100%,Maldives,1.0,f +9616,100%,Gibraltar,1.0,f +35063,100%,,2.0,t +36038,,Malta,1.0,f +41323,,Gibraltar,1.0,f +16341,90%,France,1.0,t +43865,33%,Russian Federation,2.0,f +48761,,,1.0,f +9960,,Costa Rica,1.0,f +43988,,Niue,1.0,t +39967,100%,Switzerland,1.0,f +30252,,Lithuania,1.0,t +47396,100%,Maldives,1.0,t +31938,100%,Venezuela,1.0,f +25799,100%,Uzbekistan,5.0,t +858,,,1.0,t +25879,,,1.0,f +31771,100%,Guinea,1.0,f +4416,,Brazil,1.0,f +33445,,,1.0,t +32497,,,1.0,f +5498,33%,Brazil,1.0,f +24070,,Nauru,1.0,f +26668,,Kiribati,1.0,f +32253,100%,Gambia,1.0,t +27346,,,1.0,t +13486,,,2.0,f +97,,Netherlands,1.0,t +1947,100%,Micronesia,1.0,f +46667,100%,United Kingdom,1.0,f +15057,,Malawi,1.0,f +45116,,,1.0,f +37899,,,2.0,f +23880,,,1.0,f +38762,100%,Rwanda,1.0,f +3865,,Bouvet Island (Bouvetoya),1.0,f +30996,,Tanzania,1.0,t +3089,100%,Russian Federation,1.0,f +9375,100%,Peru,2.0,t +11730,100%,Isle of Man,1.0,t +44835,100%,United Kingdom,3.0,f +13916,,Guinea,1.0,f +16542,100%,Croatia,2.0,t +35271,80%,Chad,1.0,t +37771,,Costa Rica,1.0,f +27506,100%,,1.0,t +3249,,French Guiana,1.0,t +1261,100%,Malawi,3.0,t +28000,,,1.0,t +45226,100%,Venezuela,2.0,t +47489,,Guinea,1.0,t +7633,100%,,2.0,f +46872,100%,Nicaragua,9.0,f +37657,,Turkmenistan,1.0,t +27292,,,1.0,t +47069,100%,France,1.0,f +30346,,Monaco,1.0,f +24912,100%,Sao Tome and Principe,5.0,f +39824,,Marshall Islands,1.0,f +30803,,Tonga,1.0,t +5063,,Uganda,1.0,f +11472,100%,,1.0,f +28465,,United Kingdom,1.0,f +43926,80%,Zimbabwe,2.0,t +18296,100%,,1.0,t +26435,,,1.0,t +5806,90%,Niue,2.0,f +28607,90%,Isle of Man,1.0,t +45795,75%,Denmark,1.0,f +38157,,Isle of Man,1.0,f +29087,100%,,1.0,f +351,,,1.0,f +8752,100%,,2.0,f +19467,,,1.0,t +158,,Faroe Islands,1.0,f +28517,,Lebanon,2.0,t +24116,,,1.0,f +48460,,Reunion,2.0,t +11024,,Kenya,1.0,f +12127,,Zimbabwe,1.0,f +39455,100%,Vietnam,1.0,f +10754,100%,Russian Federation,1.0,f +37367,,Kenya,1.0,t +10126,,Malta,1.0,f +31339,,,3.0,t +18872,100%,Uzbekistan,2.0,t +30788,100%,Niue,1.0,t +22528,,,1.0,t +19619,,Nicaragua,2.0,f +13022,,Pakistan,1.0,f +40972,100%,Micronesia,1.0,f +49297,,Togo,1.0,f +23924,,,1.0,f +9370,90%,Niue,2.0,t +47958,80%,Niue,1.0,f +48366,100%,Marshall Islands,1.0,f +43609,,Niue,1.0,f +40879,,Rwanda,3.0,f +8703,90%,Andorra,1.0,f +30935,,France,1.0,f +10900,100%,,1.0,t +41,100%,Portugal,2.0,f +19188,,Mauritania,1.0,f +16203,,,1.0,t +23348,,Slovakia (Slovak Republic),1.0,t +37211,,Ghana,2.0,f +20492,100%,,1.0,f +49533,50%,,1.0,t +45254,,Papua New Guinea,1.0,f +18626,100%,Nicaragua,1.0,t +696,,,1.0,f +40792,100%,,1.0,f +5935,,Maldives,1.0,t +7069,,Micronesia,1.0,f +31044,,,1.0,t +7717,100%,,1.0,f +17977,67%,Puerto Rico,1.0,f +36829,,Puerto Rico,1.0,f +24052,100%,Guinea,1.0,t +1754,,,1.0,f +12654,,Lebanon,1.0,f +47548,100%,Malta,1.0,t +26936,100%,Togo,2.0,f +15642,,Brazil,3.0,f +641,,,1.0,f +8409,98%,Maldives,9.0,f +36270,100%,Maldives,2.0,f +23785,,Kenya,1.0,f +42560,,Maldives,1.0,f +11628,,Kiribati,1.0,f +9981,100%,Anguilla,1.0,f +13437,100%,Jersey,1.0,f +11907,100%,Uzbekistan,1.0,f +26176,,El Salvador,1.0,f +20632,,Russian Federation,1.0,t +22026,,,1.0,t +42067,,,1.0,f +17708,,,1.0,f +32747,60%,Malawi,1.0,f +28569,100%,Marshall Islands,1.0,f +31365,,,1.0,f +48637,,,1.0,t +34806,100%,Vanuatu,1.0,f +34248,100%,Guinea,1.0,f +42157,,Rwanda,1.0,t +11899,,Denmark,1.0,t +39589,,Maldives,1.0,f +46474,100%,,1.0,t +9592,90%,Jersey,1.0,f +42769,,Malta,1.0,f +31982,,Uruguay,1.0,f +36130,,,1.0,t +6499,100%,,1.0,f +9805,,,1.0,f +5153,100%,France,1.0,f +35075,100%,,1.0,t +4860,,,1.0,f +38915,,,1.0,f +13944,,French Guiana,1.0,t +30874,,Mexico,1.0,t +45300,,,1.0,t +22980,,Bosnia and Herzegovina,1.0,f +41519,,Togo,1.0,f +799,,Uganda,1.0,f +23991,90%,,1.0,f +21921,,,1.0,t +29717,100%,Kiribati,3.0,f +49771,100%,Turks and Caicos Islands,4.0,f +37702,100%,Gibraltar,1.0,f +43365,,,2.0,f +48067,100%,Indonesia,1.0,f +32136,,Denmark,1.0,f +23733,,El Salvador,1.0,t +23875,63%,Chad,1.0,f +47758,,Puerto Rico,1.0,f +45955,100%,Senegal,1.0,f +28515,100%,Lebanon,1.0,t +34724,100%,Gibraltar,1.0,f +36848,,Jersey,1.0,t +20587,100%,Brazil,1.0,f +10073,,Guinea,1.0,f +31500,,Niue,1.0,f +464,,Gibraltar,1.0,f +16145,,French Guiana,1.0,f +18047,,Maldives,1.0,f +3148,90%,Rwanda,1.0,t +37227,100%,Micronesia,1.0,t +32523,,Uganda,1.0,f +39549,,French Guiana,1.0,f +13342,,Denmark,1.0,t +20518,100%,Togo,4.0,t +14887,100%,Guinea,1.0,f +10895,100%,,1.0,t +12718,100%,Indonesia,2.0,f +40453,100%,French Guiana,1.0,f +41989,100%,El Salvador,1.0,t +33126,,,1.0,f +12019,100%,,1.0,f +47081,100%,Finland,1.0,t +13319,,Tunisia,2.0,t +20740,,,2.0,t +34856,,Maldives,2.0,f +12791,,Niue,1.0,t +13788,75%,Uzbekistan,3.0,f +38132,,,1.0,t +32309,,,1.0,f +39268,,,1.0,f +4661,,,1.0,t +11809,100%,Russian Federation,1.0,f +12754,,Cocos (Keeling) Islands,1.0,f +18909,100%,,2.0,t +48532,100%,,1.0,f +40904,,Chad,2.0,f +20018,,,1.0,f +31114,100%,,2.0,t +3375,50%,Uzbekistan,2.0,t +21278,,,1.0,f +25050,,Monaco,1.0,f +29903,,Holy See (Vatican City State),1.0,t +11716,100%,,2.0,t +28941,,Sao Tome and Principe,1.0,f +2823,100%,Vietnam,1.0,t +11746,,,1.0,f +10162,,,1.0,f +46523,90%,Guinea,2.0,t +919,60%,,1.0,f +45063,100%,Isle of Man,1.0,f +30841,,,1.0,t +32240,100%,,1.0,t +305,,Rwanda,2.0,t +46448,,,1.0,f +16614,100%,Kiribati,1.0,f +3212,100%,,1.0,f +6048,100%,Tonga,1.0,f +44205,100%,Sao Tome and Principe,2.0,f +36964,,Croatia,1.0,t +12690,100%,Gambia,4.0,t +9766,,Maldives,2.0,t +17504,,,2.0,t +41436,100%,Russian Federation,1.0,f +38928,59%,,2.0,t +48359,,Niger,2.0,f +14665,100%,Cocos (Keeling) Islands,1.0,f +32823,,,1.0,f +2222,67%,Malawi,1.0,t +3272,,,1.0,t +49979,,Indonesia,1.0,t +40460,,,1.0,f +42789,,,1.0,f +18314,100%,,2.0,f +32172,,,1.0,t +3137,100%,French Guiana,1.0,f +2674,,Gambia,1.0,t +44061,100%,,1.0,f +14276,,,1.0,f +31765,100%,Estonia,1.0,f +16388,,Lithuania,1.0,f +41752,,,1.0,f +10136,100%,,1.0,f +30192,,Uzbekistan,1.0,t +15049,,Sao Tome and Principe,1.0,f +20617,,Chad,1.0,t +11744,100%,Kenya,4.0,f +23919,,,1.0,f +11885,100%,,1.0,f +2684,,Tanzania,1.0,f +10923,,Brazil,1.0,t +33376,0%,Brazil,1.0,f +8581,100%,,1.0,f +3495,,Zimbabwe,1.0,t +12264,100%,Slovakia (Slovak Republic),1.0,t +49122,0%,,1.0,f +45874,,,2.0,f +29079,,Estonia,2.0,f +20272,,Jersey,1.0,f +18262,100%,Netherlands,1.0,f +36166,,,1.0,f +32770,,,3.0,t +5688,,,1.0,f +19259,,,3.0,f +42025,100%,France,1.0,f +23806,,Indonesia,1.0,f +15038,100%,Puerto Rico,1.0,f +13039,80%,Russian Federation,2.0,t +35073,,Chad,1.0,f +152,100%,,1.0,t +21263,100%,Isle of Man,1.0,t +18087,,Uganda,2.0,t +15768,,,1.0,f +1833,,Isle of Man,1.0,f +31374,100%,,1.0,f +10602,100%,Niger,1.0,t +31364,,,1.0,f +4361,100%,,1.0,t +5957,,,1.0,f +46654,100%,Tunisia,1.0,t +10734,100%,Niue,2.0,t +24429,,Brazil,1.0,f +17963,,,1.0,f +18526,100%,,2.0,f +32182,,Costa Rica,1.0,f +26285,94%,Senegal,9.0,f +38581,,,1.0,f +33026,100%,Estonia,2.0,f +47894,100%,Turks and Caicos Islands,3.0,f +4711,,Cocos (Keeling) Islands,1.0,f +32286,100%,Kenya,2.0,t +13690,,,1.0,t +33994,,Denmark,1.0,t +41193,100%,Rwanda,1.0,f +18608,100%,Jersey,4.0,t +11975,100%,,1.0,t +40658,,,1.0,t +33232,,Russian Federation,1.0,f +4296,90%,,1.0,t +45793,100%,Niue,5.0,t +26520,,,1.0,f +25511,,,1.0,f +47195,100%,,1.0,f +18815,,Bosnia and Herzegovina,2.0,t +38847,0%,Denmark,3.0,t +13893,,Afghanistan,1.0,t +21703,91%,Palestinian Territory,1.0,t +4301,,Tonga,1.0,t +45052,,France,1.0,t +36388,,Tonga,1.0,t +41757,100%,Malta,5.0,f +49525,,Estonia,4.0,f +14492,100%,,1.0,f +11149,100%,Ecuador,1.0,t +26973,,,1.0,f +25722,0%,Micronesia,1.0,t +37877,,Tanzania,1.0,f +45192,100%,,1.0,t +16364,,,3.0,t +41982,60%,Nicaragua,2.0,f +26653,,Lebanon,1.0,t +1168,80%,,1.0,f +19706,100%,Turkmenistan,1.0,f +43692,100%,Isle of Man,1.0,t +42724,,Mauritania,1.0,t +45520,75%,,1.0,t +32257,100%,Russian Federation,2.0,t +11136,100%,,1.0,f +30977,,,1.0,f +16036,100%,Faroe Islands,1.0,f +22860,100%,Turkmenistan,2.0,t +7158,,,2.0,f +15905,100%,,2.0,f +37151,40%,Zimbabwe,1.0,f +48403,,Malta,1.0,f +43878,,Peru,1.0,f +24390,100%,Lithuania,1.0,f +44996,,Malta,1.0,f +48609,100%,Uzbekistan,2.0,f +21165,,Indonesia,1.0,t +34643,,Montserrat,1.0,f +40615,,El Salvador,1.0,f +22456,100%,,1.0,f +23829,,Uzbekistan,1.0,f +36486,,,1.0,t +19038,100%,Lebanon,3.0,t +45664,,Mauritania,1.0,f +538,100%,Guinea,1.0,f +48791,97%,Uzbekistan,5.0,t +43212,67%,,1.0,f +8070,,Faroe Islands,1.0,f +43999,100%,Niue,1.0,f +30784,,Nauru,2.0,t +1270,100%,France,1.0,f +3937,100%,Russian Federation,1.0,t +31134,100%,Maldives,2.0,t +45491,100%,France,1.0,f +13199,0%,Denmark,1.0,f +32464,,Bouvet Island (Bouvetoya),1.0,f +34474,,,1.0,f +44625,100%,Russian Federation,2.0,f +11073,,,1.0,f +38929,,Niue,1.0,f +20889,100%,Isle of Man,2.0,f +568,,Micronesia,1.0,f +44248,100%,,1.0,f +35088,,Russian Federation,1.0,f +11993,,Isle of Man,1.0,f +28550,,Ecuador,1.0,t +32137,,,1.0,t +33823,78%,France,17.0,t +15001,,Micronesia,1.0,f +47042,100%,Lebanon,2.0,t +42266,100%,,2.0,t +43722,,,1.0,t +32273,100%,Kenya,1.0,f +6898,,Monaco,1.0,t +31127,,Lebanon,1.0,t +49357,100%,Micronesia,2.0,f +33038,,Niger,1.0,f +24401,0%,Niue,3.0,f +25453,100%,,1.0,f +25313,,Mexico,1.0,t +26547,,Jersey,2.0,f +18205,100%,Cuba,21.0,f +13368,100%,,1.0,t +36698,,Gibraltar,1.0,t +18286,100%,,1.0,t +19880,100%,Greenland,1.0,f +27868,100%,,2.0,t +41803,,Vanuatu,1.0,t +17494,,Uzbekistan,1.0,f +34763,100%,Russian Federation,4.0,f +6114,,Nauru,1.0,f +35116,100%,,1.0,t +3505,,Bosnia and Herzegovina,1.0,t +40417,100%,Rwanda,1.0,f +18133,,,1.0,t +17807,90%,,1.0,f +15938,90%,,2.0,f +30822,,,1.0,t +22659,100%,,1.0,t +38501,94%,Uzbekistan,2.0,f +17101,100%,Croatia,1.0,t +29599,,,1.0,t +5317,100%,Russian Federation,1.0,f +3771,100%,,3.0,f +7547,,,1.0,t +48476,,Cape Verde,1.0,f +12534,100%,,2.0,t +27181,,,1.0,f +43736,100%,,1.0,t +10462,,Rwanda,3.0,f +19236,100%,,1.0,f +27056,100%,Kenya,4.0,f +15785,,Niue,1.0,t +40012,100%,Mexico,1.0,f +36459,,China,1.0,f +12020,100%,Congo,4.0,t +44905,,,1.0,t +46422,,,1.0,t +40938,,Tunisia,1.0,t +26648,100%,Niue,2.0,t +10295,100%,,1.0,t +470,,Bosnia and Herzegovina,2.0,t +4606,,Cocos (Keeling) Islands,1.0,f +45836,,,1.0,f +32097,100%,Denmark,5.0,f +40069,,France,1.0,t +28305,,,4.0,f +42096,100%,Senegal,1.0,f +49300,100%,Uzbekistan,1.0,f +3816,100%,United Kingdom,1.0,t +44601,100%,Costa Rica,1.0,t +6604,,,1.0,t +15542,,Denmark,1.0,f +8506,,,1.0,f +44612,100%,Jersey,2.0,t +16442,,Cuba,1.0,f +25174,,Bosnia and Herzegovina,1.0,f +19931,,,1.0,t +28093,,Papua New Guinea,1.0,t +41107,100%,Marshall Islands,6.0,f +12707,75%,Malawi,2.0,t +31311,100%,,1.0,f +20379,,,1.0,t +9287,,Nauru,1.0,f +18667,,,1.0,f +43626,,Zimbabwe,2.0,f +46802,,,1.0,f +47393,,Marshall Islands,1.0,t +11483,100%,Lebanon,1.0,f +32782,,Afghanistan,1.0,t +41537,100%,Indonesia,2.0,f +38305,,,1.0,t +14834,,Brazil,1.0,t +21240,94%,Niger,130.0,f +14479,100%,United Kingdom,1.0,t +18839,100%,,1.0,f +28852,100%,Gambia,1.0,t +41765,,France,1.0,t +42426,83%,,2.0,f +12344,100%,,1.0,f +20167,,Niue,1.0,t +18248,100%,Slovakia (Slovak Republic),2.0,f +19138,,Russian Federation,1.0,f +36535,100%,Mauritania,1.0,f +7674,,,1.0,f +20127,,France,1.0,f +31686,100%,Finland,1.0,t +36501,,Russian Federation,1.0,f +16000,100%,Reunion,2.0,f +32025,100%,Costa Rica,1.0,f +39776,,,1.0,f +27489,100%,Canada,2.0,t +1294,100%,Lebanon,1.0,f +41941,,,1.0,t +27045,,,1.0,f +9772,100%,Marshall Islands,3.0,t +20418,67%,Korea,1.0,t +8707,73%,Rwanda,21.0,t +7746,100%,Canada,2.0,t +10804,,French Polynesia,1.0,f +30196,,Marshall Islands,1.0,f +30643,,Russian Federation,1.0,f +19918,,,1.0,f +12929,,Russian Federation,1.0,t +22971,100%,Afghanistan,1.0,f +37176,70%,Isle of Man,1.0,f +918,,,1.0,t +14632,,,1.0,f +33623,100%,Nicaragua,1.0,f +16333,,,1.0,f +9681,,Vietnam,1.0,t +41379,100%,France,2.0,t +16196,,,2.0,f +20588,,Malta,1.0,t +12749,,Monaco,1.0,t +33160,90%,,1.0,t +34813,,,1.0,f +19499,,,1.0,f +9557,,Niue,1.0,f +838,100%,Cocos (Keeling) Islands,1.0,f +34057,100%,Lebanon,1.0,f +24913,,El Salvador,1.0,f +27745,100%,Lithuania,1.0,t +21538,,,1.0,f +19231,100%,Ukraine,2.0,t +24520,,,1.0,t +23629,,,1.0,f +3990,,,1.0,f +23506,,,1.0,t +17218,,Nauru,2.0,f +46950,88%,Netherlands,2.0,t +16732,100%,,1.0,f +19200,,Niue,1.0,f +40978,100%,,1.0,f +40530,,,1.0,t +19298,89%,Croatia,2.0,f +20417,,,1.0,f +39046,,Jersey,1.0,f +26596,60%,Jersey,2.0,t +393,,Lebanon,1.0,t +8057,,,1.0,f +29126,70%,,1.0,f +18629,100%,Sao Tome and Principe,3.0,f +18590,100%,Pakistan,1.0,f +10967,,Anguilla,1.0,t +44059,,Greenland,1.0,t +21011,,Russian Federation,1.0,f +34400,100%,,1.0,t +14191,100%,Slovakia (Slovak Republic),1.0,f +6922,,French Guiana,1.0,f +48349,100%,Isle of Man,2.0,f +38458,100%,Montserrat,6.0,t +33990,90%,Cocos (Keeling) Islands,1.0,f +37600,,,1.0,f +40996,100%,Kenya,1.0,f +42997,,Zimbabwe,1.0,t +14634,100%,,1.0,f +9747,,Togo,1.0,f +23046,,Zimbabwe,1.0,f +36862,,Tonga,1.0,f +21948,,Anguilla,1.0,f +46358,100%,Saint Helena,1.0,t +4680,56%,,1.0,t +46391,,,1.0,f +22686,100%,French Polynesia,1.0,t +40488,50%,,1.0,f +11238,100%,Tanzania,1.0,f +18165,,Guinea,1.0,f +45631,38%,China,1.0,t +8877,,Micronesia,1.0,f +46421,80%,Croatia,2.0,t +22934,100%,Ecuador,1.0,t +1548,100%,Jersey,1.0,t +12562,100%,,2.0,f +34966,100%,Vanuatu,1.0,f +44088,,Costa Rica,2.0,f +27779,91%,,1.0,f +2498,,United Kingdom,1.0,f +6411,70%,Gambia,1.0,f +44488,100%,Bahrain,1.0,f +25947,100%,,1.0,t +48260,,Bouvet Island (Bouvetoya),1.0,f +36184,,,1.0,t +14108,100%,Gambia,1.0,f +215,100%,Niue,1.0,f +38179,100%,Netherlands,1.0,f +11107,100%,Tanzania,1.0,f +4575,100%,Pakistan,3.0,f +21597,,,1.0,f +10182,100%,,2.0,f +46311,,Maldives,1.0,t +25793,100%,,1.0,f +31065,,,16.0,f +6720,100%,Micronesia,2.0,f +15658,100%,Afghanistan,1.0,t +21501,100%,Sao Tome and Principe,1.0,f +49622,,Jersey,1.0,t +16287,100%,,2.0,f +1867,,,1.0,t +37372,,Niue,1.0,f +49196,100%,,2.0,f +11390,,,1.0,f +47297,,,1.0,t +5343,100%,Faroe Islands,1.0,f +38593,100%,Tonga,1.0,f +38085,,Rwanda,1.0,f +32569,,French Guiana,1.0,f +29841,100%,,1.0,f +972,100%,El Salvador,1.0,t +38965,,Chile,1.0,t +28954,100%,Rwanda,4.0,f +27394,,Nauru,1.0,t +48316,100%,Peru,1.0,f +29090,100%,France,1.0,f +41468,,Tanzania,1.0,t +9331,100%,Suriname,2.0,f +5977,,,1.0,f +589,,,1.0,f +24641,,Zimbabwe,1.0,t +16414,,,19.0,f +47181,93%,Kiribati,5.0,t +7464,,Marshall Islands,1.0,t +46350,,Guinea,1.0,t +30314,100%,,1.0,f +21815,,Kenya,1.0,t +24527,,Indonesia,1.0,f +47912,100%,Tonga,1.0,f +21436,0%,Denmark,1.0,f +26224,,,1.0,t +38219,,,1.0,f +20785,63%,Chad,2.0,t +9640,100%,Denmark,2.0,t +14870,100%,Jersey,1.0,t +30331,67%,,1.0,f +31416,100%,,2.0,f +47207,,Isle of Man,1.0,f +25216,,France,2.0,t +26220,,Russian Federation,1.0,f +28315,,Puerto Rico,1.0,f +20908,100%,Maldives,1.0,t +11194,100%,Russian Federation,1.0,t +43409,100%,,1.0,t +44153,,Monaco,1.0,f +31808,,Lebanon,1.0,f +10417,100%,,1.0,f +15373,,Faroe Islands,1.0,t +27536,100%,Jersey,1.0,f +32810,100%,Russian Federation,2.0,t +18540,,Uzbekistan,1.0,t +27306,100%,Kiribati,2.0,f +22998,,Uganda,1.0,f +14287,,,1.0,f +10599,,El Salvador,1.0,f +5271,,,1.0,f +16337,,Russian Federation,2.0,t +13941,,Turks and Caicos Islands,1.0,t +34988,100%,,1.0,f +3392,,Vanuatu,1.0,f +42973,100%,,1.0,f +15567,100%,Slovakia (Slovak Republic),4.0,f +6753,100%,French Guiana,1.0,f +37597,0%,Nicaragua,1.0,f +39081,,,1.0,f +36574,100%,Russian Federation,4.0,f +15383,63%,,1.0,f +21539,,Sao Tome and Principe,1.0,t +19285,,Micronesia,1.0,f +29182,,Guinea,1.0,f +237,,Faroe Islands,1.0,t +13334,100%,,1.0,f +23369,,French Guiana,2.0,t +28417,100%,,1.0,t +38979,100%,Nicaragua,1.0,f +46152,,,1.0,t +29784,,Gibraltar,1.0,t +41166,80%,,1.0,f +10928,,Russian Federation,1.0,f +40829,,Rwanda,2.0,f +48974,,,1.0,t +33897,,,1.0,f +19878,,,1.0,t +13995,,Malawi,1.0,t +19341,100%,Guinea,1.0,f +8018,100%,Monaco,1.0,t +14369,,Micronesia,2.0,t +42506,100%,Denmark,1.0,t +38538,100%,,5.0,f +44442,100%,Russian Federation,2.0,t +35612,,,1.0,t +37523,68%,,3.0,f +38888,100%,Malta,1.0,f +49987,100%,,19.0,f +48509,,,1.0,t +5709,,Croatia,1.0,t +7367,,,1.0,t +10033,,Ghana,1.0,t +12146,,Bosnia and Herzegovina,1.0,t +21281,,,1.0,f +45817,,,1.0,f +37,,Montserrat,1.0,f +35882,,,1.0,f +4908,,Sao Tome and Principe,1.0,f +19339,,,1.0,f +22948,,Montserrat,1.0,t +11679,100%,,2.0,f +15430,,,2.0,t +9208,,,1.0,t +30504,90%,Afghanistan,2.0,f +22387,100%,Gibraltar,1.0,t +36922,,,2.0,f +3689,100%,,1.0,t +12366,100%,,1.0,f +31074,100%,Malta,4.0,t +37445,100%,Jersey,2.0,f +37111,,,1.0,f +34259,100%,Sao Tome and Principe,2.0,t +30301,,Togo,1.0,f +28832,,Nicaragua,2.0,t +1759,,Niue,1.0,t +3627,100%,Malawi,1.0,t +20838,100%,Venezuela,2.0,t +41905,,Svalbard & Jan Mayen Islands,1.0,t +14810,100%,Kiribati,1.0,f +32353,,Chile,1.0,t +48741,100%,Monaco,1.0,f +34167,,France,1.0,f +13331,100%,Fiji,1.0,f +38714,,Uganda,1.0,f +43086,100%,Monaco,1.0,t +11601,,,1.0,f +21458,,,1.0,f +30470,,Niger,1.0,f +2182,,,1.0,f +25702,,,1.0,t +36768,100%,Guinea,2.0,f +20219,,,1.0,f +25965,,Russian Federation,1.0,f +25359,,,1.0,t +35889,,,1.0,t +29123,100%,Zimbabwe,16.0,t +9829,70%,Nauru,1.0,f +24118,98%,Ghana,21.0,t +42002,,Malta,1.0,f +47664,,Nicaragua,1.0,f +43510,,Chad,2.0,f +15743,,,1.0,f +41763,,United Kingdom,2.0,t +17815,80%,Ghana,3.0,f +36910,100%,Zimbabwe,1.0,t +24117,100%,Uzbekistan,1.0,f +44751,100%,,1.0,f +31754,33%,,1.0,f +5301,70%,Gambia,1.0,f +50027,,Maldives,1.0,t +34613,,France,1.0,f +41515,90%,,1.0,f +43131,100%,Vanuatu,2.0,f +13732,100%,Maldives,1.0,f +3066,,Djibouti,1.0,f +30126,100%,Togo,5.0,t +38919,100%,,1.0,f +8804,100%,Malta,1.0,f +35593,100%,Marshall Islands,3.0,f +30798,,Marshall Islands,1.0,f +34440,,Niue,1.0,f +48575,67%,Jersey,3.0,f +7652,89%,Venezuela,2.0,f +14045,100%,Tonga,2.0,t +5917,100%,Guinea,1.0,t +4707,,Lebanon,1.0,t +26858,,,1.0,f +24976,100%,Russian Federation,1.0,t +22344,,Niue,1.0,f +3298,100%,,1.0,f +10195,100%,Croatia,1.0,t +23081,,Uzbekistan,1.0,t +9768,,,3.0,f +41622,,Puerto Rico,1.0,f +34930,,,2.0,t +16309,,,1.0,f +41364,100%,Guinea,1.0,t +46580,,Sao Tome and Principe,1.0,f +15406,80%,Guinea,1.0,f +42749,,,1.0,f +37546,100%,Rwanda,3.0,f +4970,100%,Somalia,1.0,f +36595,100%,Netherlands Antilles,2.0,f +24503,,,1.0,t +37995,,Somalia,1.0,t +36508,,Russian Federation,1.0,f +39110,100%,Jersey,1.0,t +36741,100%,Papua New Guinea,1.0,f +31994,,Mexico,1.0,f +18905,,Turkmenistan,1.0,t +36537,100%,Guinea,1.0,t +13856,60%,Afghanistan,2.0,f +41942,,Bouvet Island (Bouvetoya),1.0,t +29802,91%,Brazil,1.0,t +37853,,Isle of Man,1.0,f +16679,100%,Kenya,2.0,f +35591,,Chad,1.0,f +44107,100%,,1.0,t +14809,100%,Niger,1.0,f +10747,100%,Kiribati,2.0,t +20662,0%,Wallis and Futuna,9.0,t +18256,100%,,1.0,f +3743,100%,Kiribati,1.0,t +33527,,Tonga,1.0,t +48493,,Uzbekistan,1.0,t +47861,100%,Montserrat,1.0,f +40769,100%,Nicaragua,1.0,f +13585,,Rwanda,2.0,f +7535,,Faroe Islands,1.0,t +34237,100%,,1.0,t +48487,100%,Tunisia,2.0,t +11556,100%,Gibraltar,2.0,f +13752,,,1.0,f +26228,100%,,1.0,t +37358,0%,Uzbekistan,1.0,f +34135,100%,Isle of Man,1.0,f +30237,80%,,1.0,f +38587,,,1.0,t +32961,,United Kingdom,6.0,t +14881,100%,Brazil,2.0,t +5965,100%,,1.0,f +15087,,Zimbabwe,2.0,t +43508,,Guinea,2.0,t +29452,100%,El Salvador,1.0,t +28299,100%,Mexico,1.0,f +49,,,1.0,t +45351,90%,Liberia,3.0,f +38486,100%,Afghanistan,7.0,f +30783,,Guinea,1.0,f +35071,100%,,1.0,f +12848,100%,Denmark,2.0,f +12673,,,1.0,f +2326,,Marshall Islands,1.0,t +8645,,Niue,1.0,t +10914,,Niue,1.0,f +20388,,Guinea,1.0,f +48819,95%,Pakistan,6.0,f +40779,,,1.0,t +29146,100%,Vanuatu,2.0,t +24565,100%,Anguilla,4.0,f +48339,100%,Slovakia (Slovak Republic),2.0,f +49103,80%,Rwanda,5.0,f +29564,100%,Turkmenistan,1.0,t +49579,90%,Marshall Islands,2.0,t +12126,,Isle of Man,1.0,f +14515,,,1.0,f +29823,90%,Togo,3.0,f +10963,,Cocos (Keeling) Islands,1.0,t +25419,,,1.0,t +4636,90%,Peru,1.0,t +35796,100%,Uzbekistan,1.0,f +5647,,Bosnia and Herzegovina,4.0,t +32729,90%,Indonesia,1.0,f +37063,100%,Barbados,1.0,f +4847,100%,Indonesia,3.0,t +27448,,,1.0,f +25265,100%,Libyan Arab Jamahiriya,1.0,t +6060,100%,Christmas Island,1.0,f +41604,100%,Tonga,4.0,f +19058,100%,Mauritania,1.0,f +6949,100%,Turkmenistan,1.0,f +44339,100%,Mexico,1.0,f +2414,100%,Greenland,1.0,f +13697,,,1.0,t +19470,,Turkmenistan,1.0,t +2548,97%,Marshall Islands,3.0,f +14544,100%,,1.0,f +3334,,Indonesia,2.0,t +12932,100%,,2.0,t +26334,,United Kingdom,1.0,f +17954,100%,Niue,1.0,f +9341,100%,Russian Federation,1.0,f +46637,78%,Uganda,1.0,f +28714,100%,Monaco,1.0,t +31322,100%,,1.0,f +8286,,,1.0,f +16877,,Malta,1.0,t +19717,,United Kingdom,1.0,f +30702,100%,Uzbekistan,1.0,t +10619,,,1.0,f +3096,,,1.0,t +37416,,,1.0,t +7327,100%,Isle of Man,1.0,f +34073,,,1.0,f +27373,,Anguilla,1.0,t +24648,,Rwanda,2.0,t +1808,,Chad,2.0,t +40702,80%,Niue,2.0,t +49150,100%,United Kingdom,2.0,t +41165,100%,Lebanon,1.0,f +8223,100%,El Salvador,2.0,t +16279,100%,Uzbekistan,1.0,t +49030,100%,Kenya,2.0,f +18891,80%,,1.0,f +34279,0%,,1.0,f +38422,100%,Afghanistan,1.0,f +6645,100%,,1.0,f +39608,,United Kingdom,1.0,t +21806,89%,,1.0,f +34022,50%,Togo,1.0,f +25241,,Monaco,1.0,f +20482,100%,Malta,1.0,t +30123,100%,,2.0,f +10106,,,1.0,f +31300,100%,,1.0,f +43144,,,1.0,t +47045,100%,Uganda,2.0,t +7122,,,1.0,t +41256,100%,,1.0,f +32507,100%,,1.0,f +17256,,,1.0,f +5135,100%,Niue,1.0,f +29776,90%,Russian Federation,2.0,f +17866,100%,Tanzania,2.0,t +5181,100%,Nicaragua,2.0,f +33317,,Sao Tome and Principe,1.0,t +25728,100%,Bouvet Island (Bouvetoya),3.0,f +35250,100%,Togo,1.0,f +45865,50%,,3.0,f +35739,,Russian Federation,1.0,t +12491,100%,Pakistan,3.0,f +43635,,Moldova,1.0,f +5007,13%,Jersey,1.0,t +13402,100%,Tanzania,1.0,f +35500,100%,Niue,1.0,f +36934,,Bosnia and Herzegovina,1.0,t +25288,50%,Tanzania,1.0,f +18106,,,1.0,f +23179,90%,Denmark,1.0,f +4783,100%,Estonia,2.0,f +150,,Marshall Islands,2.0,f +14234,100%,,1.0,f +47658,100%,Micronesia,3.0,f +19469,100%,Russian Federation,1.0,f +4939,100%,Estonia,1.0,f +14573,,,1.0,t +42894,0%,Niue,1.0,f +4075,,,2.0,f +23104,,Malta,1.0,t +22776,,Micronesia,1.0,t +19532,,,1.0,f +3524,100%,Ghana,5.0,f +29633,90%,,1.0,f +1207,100%,Nicaragua,7.0,f +22764,100%,,1.0,f +24006,,,1.0,f +24654,,,1.0,t +4117,,Guinea,1.0,f +8,,Denmark,2.0,t +26122,,Nicaragua,2.0,f +43718,,,1.0,t +29163,100%,Nicaragua,1.0,f +2889,100%,Uganda,1.0,t +19705,100%,,1.0,f +10221,,,1.0,f +23503,100%,Marshall Islands,1.0,f +25439,,,1.0,t +13213,,Lebanon,1.0,f +23580,60%,,1.0,f +29727,100%,Senegal,2.0,f +14987,100%,,1.0,t +29546,100%,French Polynesia,1.0,f +10682,100%,Isle of Man,1.0,f +21070,,,1.0,f +8442,100%,Tunisia,2.0,t +41342,100%,,2.0,f +2861,90%,Niue,1.0,t +24325,,,1.0,f +38493,,Uzbekistan,12.0,f +4129,100%,Niue,3.0,t +10760,,Ghana,1.0,f +38972,78%,Tunisia,1.0,f +30945,,Niue,1.0,f +31128,100%,Montserrat,2.0,t +42652,,,1.0,f +37393,,Costa Rica,1.0,f +49843,100%,,1.0,f +50049,,,1.0,f +46906,100%,,1.0,t +9234,100%,Sao Tome and Principe,1.0,f +29372,,Canada,1.0,f +11540,,French Guiana,1.0,t +18269,86%,Fiji,12.0,f +3884,70%,Tanzania,1.0,f +7924,60%,,1.0,t +31411,100%,Denmark,1.0,f +40181,100%,Russian Federation,2.0,t +47381,,Indonesia,3.0,f +6594,100%,Cuba,2.0,t +31327,,Russian Federation,1.0,t +12862,100%,United Kingdom,1.0,f +23560,93%,,9.0,f +35123,100%,Chile,1.0,f +25168,100%,Chile,1.0,t +26826,100%,Brazil,1.0,f +31032,,,2.0,t +24858,100%,Indonesia,2.0,t +14015,,,1.0,t +31156,100%,,1.0,t +37169,100%,Jersey,2.0,f +1130,,Mauritania,1.0,f +41837,100%,Brazil,2.0,t +42534,100%,Marshall Islands,2.0,t +8410,75%,Guinea,1.0,t +35735,,Rwanda,1.0,f +6896,,Barbados,2.0,t +4442,,Turks and Caicos Islands,1.0,f +41148,100%,Cape Verde,1.0,f +7840,100%,Kenya,1.0,t +33595,,,1.0,t +19671,,Australia,1.0,t +32850,,Uganda,1.0,f +16327,100%,Russian Federation,1.0,t +35229,88%,Marshall Islands,1.0,f +41378,100%,Kiribati,1.0,f +32686,78%,Micronesia,2.0,t +46639,,,1.0,f +282,100%,Sao Tome and Principe,2.0,t +1257,100%,,2.0,f +24713,,Lebanon,1.0,t +34039,100%,Brazil,1.0,f +29895,,,1.0,f +37821,,United Kingdom,1.0,t +10514,,Gambia,1.0,f +12333,100%,,1.0,t +19679,,,1.0,f +45144,100%,,1.0,f +12494,,Ghana,3.0,t +15432,,Gibraltar,1.0,f +44695,,,1.0,t +42867,100%,Indonesia,2.0,t +29563,100%,Jersey,2.0,f +6415,,Niue,1.0,f +79,0%,Turkmenistan,1.0,f +18454,100%,Nicaragua,5.0,t +18979,100%,Tonga,6.0,t +22349,0%,Monaco,1.0,f +25791,100%,,2.0,f +15099,,,1.0,f +46928,,Tanzania,1.0,t +29762,0%,Brazil,1.0,t +4750,,Guernsey,1.0,f +9424,100%,Peru,1.0,f +38255,25%,Hong Kong,4.0,t +6725,,Lebanon,2.0,t +26464,100%,Kiribati,1.0,t +31042,100%,Guinea,1.0,t +14069,100%,Mauritania,1.0,t +36864,,,1.0,t +43177,,Nauru,1.0,f +34788,,Niue,1.0,t +16385,100%,,1.0,t +19785,,Kenya,4.0,f +25056,100%,Gibraltar,1.0,t +2854,100%,Venezuela,1.0,f +29218,,Marshall Islands,1.0,f +20461,,Russian Federation,1.0,f +35628,,,1.0,t +48308,,United Kingdom,1.0,f +8085,,,1.0,f +21298,100%,Uzbekistan,2.0,f +28905,,,1.0,t +38282,,Tonga,5.0,f +3442,100%,Tonga,2.0,t +42954,100%,Finland,1.0,f +1215,100%,Sao Tome and Principe,1.0,f +28341,,France,1.0,t +38404,0%,Zimbabwe,1.0,f +13674,90%,Fiji,2.0,t +6833,,Jersey,1.0,f +1162,,,2.0,f +20071,,Sao Tome and Principe,1.0,t +46895,,Turks and Caicos Islands,1.0,f +41332,100%,,4.0,f +34484,,Isle of Man,1.0,f +25665,,,1.0,f +36340,,Marshall Islands,1.0,f +25990,50%,Venezuela,1.0,t +17106,100%,,1.0,f +117,,Tonga,1.0,t +33027,,,1.0,f +49047,,Djibouti,1.0,t +37739,,,1.0,t +32001,100%,Svalbard & Jan Mayen Islands,2.0,t +40841,100%,Micronesia,1.0,f +41891,,,1.0,t +35782,,Turkmenistan,1.0,t +31957,,Russian Federation,1.0,t +23234,100%,Marshall Islands,1.0,f +1433,,Rwanda,1.0,f +32194,,China,1.0,f +12508,70%,Holy See (Vatican City State),3.0,t +27293,75%,Maldives,1.0,f +21010,,,1.0,f +20868,100%,Montserrat,2.0,t +38547,0%,Angola,3.0,t +6678,,China,1.0,f +5040,100%,Kenya,1.0,f +14716,,Faroe Islands,2.0,t +17756,,Tonga,1.0,f +41178,50%,Barbados,1.0,t +18439,,,1.0,t +9177,,Maldives,1.0,f +7190,,Greenland,1.0,f +10639,100%,Mauritania,1.0,f +3637,80%,,2.0,f +23512,,,1.0,f +21385,100%,Cuba,10.0,f +20420,0%,Jersey,1.0,t +14942,,Malta,1.0,f +26349,100%,,1.0,t +42301,100%,Turkmenistan,1.0,t +4417,100%,Niue,1.0,f +20226,,,1.0,f +20217,90%,,2.0,t +7318,100%,Niger,2.0,f +24697,100%,Congo,1.0,t +4134,,Niue,1.0,f +9973,,,1.0,f +41898,,Maldives,1.0,t +46049,100%,El Salvador,2.0,f +35658,,Zimbabwe,1.0,f +29566,,French Polynesia,1.0,f +44827,,Senegal,1.0,f +27834,,,1.0,t +8110,,Papua New Guinea,1.0,f +34759,,United Kingdom,1.0,f +2035,40%,,1.0,f +37008,,Cape Verde,1.0,t +7769,100%,Niue,1.0,f +9481,,Somalia,1.0,f +29502,,Tonga,1.0,t +6742,100%,,1.0,f +29327,,Svalbard & Jan Mayen Islands,1.0,f +33430,100%,Monaco,2.0,f +37483,100%,Gibraltar,3.0,f +3672,,Bouvet Island (Bouvetoya),1.0,t +405,,United Kingdom,2.0,t +48662,,Bahrain,1.0,t +3978,100%,,1.0,t +26681,100%,Niue,1.0,f +27767,,Niue,1.0,f +26398,,,1.0,f +12604,,Bosnia and Herzegovina,1.0,t +21153,,Micronesia,1.0,f +34772,,Congo,1.0,f +36044,,Bouvet Island (Bouvetoya),1.0,f +17529,100%,Turkmenistan,1.0,f +8861,100%,Tunisia,2.0,t +21607,90%,France,3.0,t +34955,43%,Russian Federation,1.0,t +3414,,,1.0,f +48554,100%,Gibraltar,1.0,f +36426,,United Kingdom,1.0,t +17679,100%,Togo,1.0,f +43017,0%,Gibraltar,2.0,t +39895,100%,,3.0,f +21077,100%,Togo,1.0,t +3503,100%,Ukraine,2.0,t +48615,,Netherlands,1.0,f +10357,100%,Indonesia,1.0,t +24061,,Zimbabwe,1.0,f +6298,,Brazil,1.0,f +25225,,Ukraine,1.0,f +35854,100%,Mexico,1.0,f +23158,100%,,1.0,f +16894,100%,,1.0,f +34107,,Costa Rica,1.0,f +5572,90%,,1.0,f +28838,,,1.0,t +40374,100%,Ecuador,1.0,t +45090,100%,,1.0,f +31558,33%,Niger,1.0,f +27399,100%,,4.0,f +44325,,Lebanon,1.0,t +27934,100%,,1.0,f +44295,100%,Djibouti,1.0,f +35279,100%,Guinea,1.0,f +43236,,,1.0,f +47143,100%,Anguilla,1.0,t +27611,,Brazil,1.0,f +12942,,Guinea,3.0,t +30806,70%,Guernsey,1.0,f +20203,,,1.0,f +31341,,Russian Federation,2.0,f +45122,100%,,1.0,t +24970,100%,Cape Verde,1.0,t +21363,100%,Guinea,2.0,t +35022,,Turkmenistan,1.0,f +27588,,,1.0,t +10035,100%,Tonga,2.0,f +22246,,,1.0,t +7561,100%,Venezuela,6.0,t +26396,,,2.0,f +39241,,Rwanda,1.0,f +32539,90%,,1.0,f +23456,,,1.0,t +42625,,Russian Federation,1.0,t +36978,,,1.0,f +44843,,Croatia,1.0,t +32996,,,1.0,f +9876,,Costa Rica,1.0,f +46894,100%,Malawi,2.0,t +34689,,French Guiana,1.0,t +16913,100%,,2.0,t +8716,80%,Sao Tome and Principe,2.0,t +36143,90%,Turks and Caicos Islands,4.0,f +35036,100%,Anguilla,1.0,f +14024,100%,,2.0,f +15988,,Lebanon,1.0,t +13019,100%,,1.0,f +14955,100%,Micronesia,1.0,t +11345,,Tunisia,1.0,t +10495,,Brazil,1.0,f +13988,100%,,1.0,f +29330,,El Salvador,2.0,t +224,100%,,1.0,t +19517,,Malawi,1.0,f +33189,,French Guiana,1.0,f +24024,80%,,3.0,f +46379,100%,,2.0,f +21821,,,1.0,t +12057,,Sao Tome and Principe,2.0,f +4585,,Philippines,1.0,t +29302,,,1.0,f +18848,,,3.0,t +7191,100%,Uzbekistan,2.0,f +36897,,Tonga,1.0,t +6223,100%,Marshall Islands,12.0,f +7852,100%,Faroe Islands,1.0,f +27655,,Monaco,1.0,f +40940,100%,Malta,2.0,f +38933,100%,Montserrat,1.0,f +18578,,Sao Tome and Principe,1.0,t +19137,100%,Russian Federation,1.0,t +37574,100%,Zimbabwe,2.0,t +7570,100%,Uzbekistan,3.0,f +28074,70%,,3.0,t +8138,,Lebanon,8.0,f +44405,100%,France,1.0,t +32179,67%,,1.0,f +43688,100%,,1.0,f +30070,100%,,1.0,t +39875,,,1.0,f +32948,100%,Croatia,1.0,t +30680,,Guinea,4.0,f +19826,100%,,2.0,f +42966,100%,Zimbabwe,1.0,t +8347,100%,,1.0,f +17758,80%,,1.0,f +10212,,Turkmenistan,2.0,f +32619,100%,,1.0,t +29135,,Gambia,1.0,t +45897,,Cook Islands,1.0,t +34687,100%,,2.0,f +26339,,,1.0,f +12045,100%,,2.0,t +8936,,Cocos (Keeling) Islands,1.0,f +29375,,Jersey,1.0,t +15496,50%,,1.0,t +42843,100%,,1.0,f +36930,100%,Sao Tome and Principe,1.0,t +31591,100%,,1.0,t +27146,,Russian Federation,1.0,f +3238,100%,,1.0,t +16146,100%,,1.0,f +7402,100%,,1.0,f +12117,100%,Monaco,1.0,f +13105,100%,Anguilla,2.0,f +28736,,Senegal,1.0,t +45770,,,1.0,f +12087,,Jersey,2.0,f +22781,100%,Croatia,1.0,f +8862,100%,Nicaragua,3.0,f +12593,100%,Reunion,2.0,f +636,97%,Vanuatu,3.0,f +36836,,Tanzania,1.0,f +47728,,Jersey,1.0,f +7552,,,2.0,f +24738,100%,Somalia,2.0,t +14040,,,1.0,t +10058,100%,Nicaragua,1.0,f +1410,100%,,1.0,f +21228,90%,Russian Federation,1.0,f +4794,100%,Marshall Islands,1.0,f +11959,100%,Gibraltar,2.0,t +4660,,Gambia,1.0,f +17294,,,1.0,f +7868,100%,Marshall Islands,1.0,f +42285,100%,Chile,1.0,t +48601,,,3.0,f +28445,100%,Jersey,1.0,t +23266,,Lebanon,1.0,f +42827,100%,Nicaragua,3.0,t +11296,100%,Micronesia,1.0,f +14767,,,1.0,f +8700,,,1.0,t +40254,,,1.0,t +195,,Slovenia,1.0,f +4068,100%,Togo,1.0,f +6010,100%,Isle of Man,1.0,t +46879,100%,Netherlands,1.0,f +3277,,,1.0,f +29615,100%,Zimbabwe,2.0,t +43883,,Russian Federation,1.0,t +48158,100%,Canada,1.0,t +47059,,Lebanon,1.0,f +38819,,,1.0,f +34839,,Jersey,1.0,t +49049,50%,Nauru,1.0,f +31996,,Niue,1.0,f +30862,,Tunisia,2.0,t +10064,,Russian Federation,1.0,t +6774,,,1.0,t +42097,100%,Kenya,1.0,f +17521,100%,,1.0,f +28156,100%,Lebanon,2.0,f +32068,100%,,1.0,f +5890,,China,1.0,f +28992,100%,Jersey,1.0,t +10836,,Croatia,1.0,t +49769,,El Salvador,1.0,t +31116,,Sao Tome and Principe,1.0,f +10158,100%,Ukraine,6.0,t +16360,,,2.0,f +24707,,,1.0,f +42563,90%,Micronesia,3.0,t +12348,,,1.0,f +25177,100%,Costa Rica,2.0,t +35517,,Guinea,1.0,f +27333,100%,Netherlands,1.0,t +15968,100%,,1.0,f +8657,,Kiribati,1.0,f +27094,,,1.0,f +30359,100%,Bosnia and Herzegovina,1.0,f +6921,100%,,1.0,f +24911,100%,,1.0,f +12373,,China,1.0,t +1968,100%,,1.0,t +36839,100%,,1.0,t +38356,100%,Anguilla,2.0,f +30985,100%,Uzbekistan,5.0,f +40656,100%,Sao Tome and Principe,1.0,f +15754,,Rwanda,1.0,f +5359,,Moldova,1.0,f +8749,100%,Niue,1.0,f +7241,,Kiribati,1.0,f +33416,100%,,1.0,t +6312,100%,Kenya,1.0,f +21987,,,1.0,f +23682,57%,Uganda,4.0,f +24273,,,1.0,f +31806,100%,Micronesia,1.0,t +44961,0%,,1.0,f +12234,,Cocos (Keeling) Islands,1.0,f +13123,,Kenya,1.0,f +44532,0%,Micronesia,1.0,f +43178,100%,Marshall Islands,2.0,f +3032,75%,Andorra,1.0,f +21464,100%,Mauritania,1.0,f +32089,100%,Cuba,1.0,f +15458,100%,,2.0,f +33945,100%,,2.0,t +30797,90%,Tunisia,1.0,f +30094,100%,,1.0,t +18154,,Finland,1.0,t +35205,100%,Uzbekistan,1.0,f +46901,,,1.0,t +47252,,Afghanistan,2.0,f +12495,,Lebanon,1.0,t +35764,,,2.0,t +24606,,,1.0,f +34636,90%,Isle of Man,1.0,t +13668,,Niue,1.0,f +39997,,,1.0,f +32079,,Suriname,1.0,f +25713,,,1.0,f +30209,100%,,1.0,f +47665,,Kenya,4.0,t +10491,,Guinea,2.0,t +21113,,Faroe Islands,1.0,t +32293,,El Salvador,1.0,t +33632,,Monaco,1.0,t +48522,,,1.0,f +6280,,Montserrat,1.0,f +22515,,,1.0,t +40997,,Maldives,1.0,t +26539,100%,,1.0,t +24031,,Chile,1.0,t +41548,,Chile,2.0,t +30829,,,1.0,f +42711,,,1.0,t +13362,,,1.0,t +37222,,,1.0,f +13894,100%,Isle of Man,5.0,f +12738,100%,Ecuador,2.0,t +45946,100%,,1.0,f +46361,,Chad,1.0,t +23534,100%,Maldives,2.0,f +49943,100%,,1.0,f +7184,77%,Reunion,13.0,f +17159,100%,,1.0,f +44877,75%,Tunisia,1.0,t +37155,,Niue,1.0,f +5819,,United Kingdom,1.0,f +47682,100%,Niue,1.0,t +18960,92%,,1.0,f +40104,,Somalia,1.0,t +25484,100%,,1.0,f +7890,,Micronesia,1.0,f +26655,100%,,1.0,t +28323,,Malta,1.0,t +2958,,,1.0,f +3294,100%,Uganda,3.0,f +13277,100%,Rwanda,2.0,t +9756,,Greenland,1.0,f +16780,,,1.0,f +8643,,Cocos (Keeling) Islands,1.0,f +10143,100%,Sao Tome and Principe,2.0,f +33540,,,1.0,t +19730,,Bahrain,1.0,t +34332,,Nicaragua,1.0,f +31904,,Niue,3.0,t +16942,100%,Uganda,2.0,f +10278,,China,1.0,f +26780,100%,Guinea,7.0,f +14622,,Isle of Man,1.0,t +25756,100%,Nicaragua,2.0,f +42466,100%,Wallis and Futuna,1.0,t +40467,,,1.0,f +37427,,,1.0,t +39700,100%,,1.0,f +10005,,Turkmenistan,1.0,t +15600,0%,Marshall Islands,1.0,f +9192,,Tunisia,1.0,t +19029,,,1.0,t +49832,100%,Bouvet Island (Bouvetoya),1.0,f +48335,,,1.0,f +48792,,Faroe Islands,1.0,t +43066,100%,,1.0,t +15692,,Lebanon,1.0,f +2535,100%,France,2.0,t +40849,,,1.0,f +13030,100%,Mauritania,1.0,t +45291,100%,Holy See (Vatican City State),1.0,t +40320,100%,Niue,2.0,f +40480,100%,Lebanon,1.0,t +11220,,Cape Verde,1.0,f +19526,100%,Malta,2.0,f +41288,100%,Nicaragua,3.0,f +30125,,,1.0,f +729,100%,Saint Helena,3.0,f +28051,100%,Marshall Islands,1.0,f +5659,100%,Tanzania,1.0,t +19754,0%,Indonesia,1.0,t +17765,,,1.0,t +38488,,Malta,1.0,t +23466,100%,Anguilla,1.0,t +29699,90%,Zimbabwe,1.0,f +48502,100%,Nicaragua,11.0,t +18644,,,1.0,f +8980,,Mauritania,1.0,t +4698,,United Kingdom,1.0,t +2315,,Marshall Islands,1.0,t +46017,75%,,1.0,f +4972,100%,,1.0,t +23000,90%,,4.0,f +32556,90%,Estonia,11.0,t +17212,,Marshall Islands,1.0,f +7109,100%,Malta,1.0,f +8550,100%,Jersey,1.0,t +17024,,Niue,1.0,t +10207,100%,Anguilla,1.0,t +35640,100%,Uganda,3.0,f +26084,,Vietnam,4.0,t +37776,,,1.0,t +34831,100%,Denmark,2.0,f +17921,100%,Cuba,1.0,t +4267,100%,Senegal,22.0,t +14190,,,1.0,t +12162,82%,,2.0,f +6801,100%,Kiribati,2.0,f +22311,100%,,1.0,t +20001,,,1.0,f +24918,100%,Micronesia,1.0,f +38741,,France,1.0,f +13101,100%,Jersey,1.0,t +21478,,Malta,1.0,t +18298,0%,Uzbekistan,1.0,f +47853,,,2.0,t +17569,,,2.0,f +23938,100%,Guernsey,1.0,f +29736,100%,,1.0,t +18759,,,1.0,f +2768,100%,Christmas Island,2.0,t +19899,,Slovenia,1.0,t +20520,83%,,1.0,f +19842,85%,Turkmenistan,53.0,f +25352,100%,Monaco,3.0,t +41407,,,1.0,f +19647,,Malawi,1.0,t +38945,,,1.0,t +47761,100%,Niue,2.0,f +30486,,Micronesia,1.0,f +14930,,,1.0,t +20615,,,1.0,f +21646,100%,Togo,2.0,f +35246,100%,,2.0,f +35412,,,1.0,t +4991,,,1.0,f +28444,,Chad,1.0,t +9734,,Togo,1.0,f +26315,,El Salvador,1.0,t +24844,100%,Uzbekistan,6.0,f +12425,,Guernsey,1.0,f +42526,100%,Niue,1.0,t +2916,100%,Nicaragua,1.0,t +10868,0%,,1.0,t +9541,,,1.0,t +9894,100%,,1.0,f +13301,100%,Togo,2.0,f +24853,100%,,1.0,f +38079,,French Guiana,1.0,f +24133,100%,Cuba,1.0,t +13042,100%,Costa Rica,5.0,f +8029,100%,Djibouti,1.0,f +16742,83%,Monaco,1.0,f +9922,100%,Papua New Guinea,1.0,f +33820,,,1.0,t +8945,,Guinea,1.0,f +48838,,Niue,1.0,f +37003,,France,1.0,f +43051,,,1.0,f +39014,33%,Nicaragua,1.0,f +562,100%,Nicaragua,1.0,f +35368,100%,Kiribati,1.0,f +39818,,,1.0,f +45821,100%,Nicaragua,1.0,t +28087,,Rwanda,1.0,f +19283,,Niue,1.0,f +23194,100%,Ecuador,1.0,t +6266,100%,,4.0,f +41494,,Guinea,1.0,t +10215,100%,,1.0,f +42539,,Faroe Islands,1.0,f +33032,83%,Russian Federation,1.0,f +49330,100%,Indonesia,1.0,f +45754,100%,French Guiana,1.0,t +47223,,Cocos (Keeling) Islands,1.0,f +37915,,Marshall Islands,1.0,f +18981,,Libyan Arab Jamahiriya,1.0,t +4471,67%,,2.0,f +38723,100%,Bosnia and Herzegovina,1.0,f +13553,93%,Vietnam,5.0,f +46335,100%,Nicaragua,1.0,t +31117,80%,Croatia,3.0,t +31836,100%,Zimbabwe,1.0,f +30432,0%,,1.0,f +38618,100%,Guernsey,1.0,t +13965,,,1.0,f +22930,,Nicaragua,1.0,f +794,100%,Indonesia,1.0,f +28763,67%,Rwanda,2.0,f +11239,,,1.0,f +4239,,,1.0,f +1300,100%,Micronesia,1.0,f +15446,,Zimbabwe,1.0,f +33113,100%,,2.0,t +44115,,Isle of Man,1.0,f +797,100%,,1.0,f +44376,,,3.0,t +17589,80%,,1.0,f +37019,,,1.0,f +40542,100%,,1.0,t +7124,,,1.0,f +43347,100%,Bouvet Island (Bouvetoya),1.0,f +39807,50%,Ecuador,1.0,f +33822,,Chad,1.0,f +5681,75%,Denmark,1.0,f +21146,,,1.0,f +25164,,Kenya,4.0,f +14169,,,1.0,f +26015,86%,Russian Federation,1.0,t +22492,60%,Kiribati,2.0,f +42286,94%,Vietnam,9.0,t +4588,100%,Malawi,1.0,t +46764,100%,Chile,3.0,t +45787,100%,,1.0,f +30453,,Svalbard & Jan Mayen Islands,1.0,f +40202,100%,,2.0,f +27854,,,1.0,f +44291,,Kiribati,1.0,f +41972,,,1.0,t +6566,100%,French Guiana,1.0,t +29659,,Tonga,1.0,f +31104,100%,France,2.0,f +32531,100%,Togo,4.0,f +41232,100%,,3.0,f +734,,Lebanon,1.0,t +36600,,,1.0,f +16929,100%,,1.0,t +22294,,Sao Tome and Principe,1.0,f +15837,100%,,1.0,t +32588,,Nicaragua,1.0,t +24235,,Libyan Arab Jamahiriya,1.0,t +4056,,Brunei Darussalam,1.0,f +41160,,Papua New Guinea,1.0,t +8492,,,1.0,t +28316,100%,,2.0,f +3193,100%,Latvia,2.0,f +35914,100%,China,1.0,t +44450,,,1.0,f +11453,,,1.0,t +49089,,Denmark,1.0,f +10950,100%,Greenland,1.0,f +10336,,,1.0,t +47163,,,1.0,f +35104,100%,Switzerland,2.0,f +12583,100%,,1.0,f +23684,,,1.0,t +996,,Cuba,1.0,t +32380,,,1.0,f +34981,,Turks and Caicos Islands,3.0,f +28965,90%,Pakistan,2.0,f +39104,,Togo,1.0,f +2492,,Russian Federation,1.0,f +13716,80%,,1.0,f +38267,90%,United Kingdom,1.0,t +21967,100%,Cape Verde,1.0,f +18869,,Malta,2.0,f +16596,100%,Congo,3.0,f +18913,,,1.0,t +22791,100%,Uzbekistan,1.0,t +17881,100%,,1.0,t +26767,87%,,2.0,f +42643,,,1.0,f +40549,100%,Maldives,2.0,t +8173,100%,Barbados,2.0,t +1850,,,1.0,f +40927,100%,Micronesia,3.0,f +20943,100%,Jersey,2.0,f +13977,90%,Isle of Man,1.0,f +12683,100%,Lithuania,1.0,t +47552,,China,1.0,f +37184,100%,Russian Federation,1.0,t +43277,,Sao Tome and Principe,1.0,f +29320,,,1.0,f +48748,,Micronesia,1.0,t +36830,,Ghana,1.0,f +2255,100%,Guinea,1.0,t +16425,,Guinea,1.0,f +2931,100%,,1.0,f +21330,,Zimbabwe,1.0,t +36319,75%,France,2.0,t +44970,91%,Russian Federation,1.0,f +22336,,Venezuela,1.0,f +13234,,Niue,2.0,f +6729,,,1.0,f +22451,100%,Guinea,1.0,f +45814,,Monaco,1.0,f +48475,100%,Lebanon,1.0,f +18219,100%,Jersey,1.0,t +49232,100%,,1.0,f +42431,,,1.0,t +27485,100%,Uzbekistan,2.0,t +47549,100%,,1.0,f +34337,100%,Croatia,2.0,f +25770,,Micronesia,1.0,f +7923,100%,Togo,2.0,t +20561,,,1.0,f +30502,,,1.0,f +18710,,Costa Rica,1.0,f +42008,,,1.0,t +49081,,,1.0,f +32164,,Niue,1.0,f +32480,,Croatia,2.0,t +17972,100%,Guinea,1.0,f +6123,,Gambia,1.0,t +28685,100%,Guinea,1.0,t +24519,,Saint Martin,2.0,f +17481,,Anguilla,3.0,t +21950,,Uzbekistan,1.0,t +21909,100%,Christmas Island,3.0,f +5798,100%,,1.0,f +22074,,Jersey,1.0,f +15139,,,1.0,f +43593,100%,Guernsey,1.0,t +34410,100%,Maldives,1.0,f +44504,100%,,3.0,f +21573,100%,Kiribati,1.0,f +22005,100%,,1.0,f +10961,100%,Guinea,1.0,f +35574,100%,,1.0,f +10039,100%,Vanuatu,6.0,t +27316,100%,Puerto Rico,5.0,f +49213,,Micronesia,2.0,t +42028,50%,,1.0,t +28588,,,1.0,t +42796,,,1.0,f +6200,100%,,3.0,t +3854,100%,Philippines,3.0,f +19193,,El Salvador,1.0,t +24442,,Tonga,1.0,t +22359,100%,,3.0,f +33144,,Djibouti,3.0,t +37605,,,4.0,f +33518,100%,France,1.0,f +4005,100%,French Guiana,2.0,f +46930,100%,Turks and Caicos Islands,1.0,f +19020,,Zimbabwe,1.0,f +45099,,Mexico,1.0,f +19361,,Bosnia and Herzegovina,1.0,t +26234,,,1.0,t +19390,,Nicaragua,1.0,f +30992,,Turkmenistan,1.0,f +14240,100%,Chile,3.0,t +763,,,1.0,f +9600,,Brazil,1.0,f +23874,100%,Denmark,1.0,f +2730,,,1.0,f +21912,,Brazil,1.0,f +42478,,Costa Rica,1.0,f +773,100%,,1.0,f +41856,100%,Maldives,13.0,t +11571,,Guinea,1.0,t +935,,Guernsey,1.0,t +28230,,French Guiana,1.0,t +14762,,Nicaragua,1.0,f +22966,,Finland,1.0,t +41254,100%,Niue,1.0,f +49565,92%,Uganda,14.0,f +31519,,,1.0,t +48294,,Papua New Guinea,1.0,f +1977,,,1.0,t +35560,,Gambia,2.0,f +8570,,Jersey,1.0,f +12698,,Barbados,1.0,t +36375,100%,Mauritania,1.0,f +2408,90%,,1.0,f +42275,,Niue,1.0,f +14602,100%,Malawi,1.0,t +15561,,Marshall Islands,1.0,f +14565,80%,Isle of Man,1.0,f +15232,,Russian Federation,2.0,t +38497,100%,Tunisia,1.0,f +3752,,Lithuania,1.0,f +20405,,,1.0,f +41592,100%,Svalbard & Jan Mayen Islands,1.0,f +18568,,Russian Federation,1.0,f +40777,100%,,1.0,f +35405,100%,Tanzania,1.0,f +10803,100%,Nicaragua,1.0,f +21701,100%,,1.0,f +14009,100%,Russian Federation,1.0,f +26171,100%,Uruguay,1.0,f +35499,94%,Andorra,4.0,f +23256,100%,,6.0,f +17270,100%,Niue,1.0,t +24770,,,1.0,f +18120,100%,,1.0,t +44477,90%,Maldives,3.0,t +41448,,,1.0,t +9704,,,1.0,t +35363,,,1.0,f +33572,,,1.0,f +35886,,,2.0,f +29119,100%,Nicaragua,2.0,f +46609,100%,,4.0,t +46138,78%,Niger,1.0,f +20126,,Vanuatu,1.0,t +49114,,Venezuela,1.0,f +7777,,Pakistan,2.0,f +9892,,France,2.0,f +5252,,Kenya,1.0,t +42121,,Canada,1.0,t +16252,100%,,1.0,f +25335,,Croatia,1.0,t +24162,50%,,1.0,f +15164,100%,Tanzania,2.0,t +9200,,Russian Federation,1.0,t +29512,100%,Guinea,1.0,t +7960,,Peru,1.0,t +29516,,Chad,1.0,t +48794,75%,,1.0,f +21116,,,1.0,f +36916,0%,Uzbekistan,1.0,f +40144,100%,,1.0,f +42158,100%,,15.0,f +38747,,Zimbabwe,1.0,t +37303,,,1.0,f +42519,,Nauru,1.0,t +28830,100%,Lithuania,2.0,f +7972,100%,Uzbekistan,2.0,t +16755,,France,1.0,f +35375,,,1.0,f +45157,,Ghana,1.0,t +21853,100%,El Salvador,1.0,t +25597,,,5.0,f +6049,100%,Isle of Man,1.0,f +33283,100%,Isle of Man,1.0,t +31937,100%,,1.0,f +35136,,,1.0,f +49732,,,1.0,f +24135,70%,,1.0,f +44137,50%,,1.0,t +22538,100%,Gambia,1.0,t +25590,100%,Gambia,1.0,t +14635,,Monaco,1.0,f +37401,60%,Russian Federation,1.0,f +15728,100%,Lithuania,1.0,t +37999,100%,Senegal,20.0,t +13055,100%,,1.0,f +30778,100%,Guernsey,1.0,t +41596,100%,United Kingdom,1.0,t +24010,100%,Turkmenistan,2.0,t +42570,,,1.0,f +32186,100%,United Kingdom,2.0,f +23039,100%,,1.0,f +28527,100%,Russian Federation,1.0,t +39183,75%,,1.0,f +22709,,Jersey,1.0,f +6992,100%,Isle of Man,1.0,f +10132,,,1.0,t +8127,67%,Turks and Caicos Islands,1.0,f +22945,,,1.0,f +47738,100%,,1.0,f +13179,77%,Marshall Islands,2.0,f +18697,100%,Rwanda,8.0,f +16162,100%,Svalbard & Jan Mayen Islands,2.0,t +17753,,Guinea,1.0,f +18737,,,1.0,f +18886,95%,Vietnam,3.0,t +21679,100%,China,2.0,t +45108,,Holy See (Vatican City State),1.0,f +43302,100%,Reunion,1.0,t +7871,100%,Jersey,1.0,f +30087,,Bahrain,1.0,f +38842,100%,Uzbekistan,1.0,f +48160,,,1.0,f +30205,100%,,1.0,f +29022,100%,,1.0,f +40286,50%,Monaco,1.0,t +7101,,Isle of Man,1.0,f +33357,,Denmark,1.0,t +4633,100%,Benin,4.0,f +6075,,,1.0,f +2105,100%,Tonga,1.0,f +1578,,French Guiana,1.0,f +49677,,,1.0,t +2475,,,1.0,t +48241,33%,Russian Federation,1.0,t +35721,100%,,1.0,f +21317,100%,Maldives,2.0,f +38904,100%,,1.0,f +47626,100%,,1.0,f +36961,29%,,1.0,f +23952,,,1.0,t +19415,100%,France,2.0,f +15540,100%,Russian Federation,1.0,f +47152,,Isle of Man,2.0,f +20250,100%,Brazil,2.0,t +18705,100%,Nicaragua,2.0,f +26850,,Somalia,1.0,t +43472,,Korea,1.0,f +8947,,Montserrat,2.0,f +49948,,Lebanon,1.0,t +46318,100%,Rwanda,1.0,f +6646,100%,,1.0,f +16512,100%,,2.0,t +32644,100%,Niue,1.0,f +33313,,Djibouti,1.0,f +14705,,Puerto Rico,1.0,f +8778,100%,Somalia,1.0,t +21927,100%,Brazil,1.0,f +45305,,,1.0,t +33343,100%,Vanuatu,1.0,t +6784,100%,Lebanon,1.0,t +20244,100%,,1.0,t +643,,,1.0,f +46269,,Monaco,1.0,t +45809,,,1.0,t +455,100%,Uganda,5.0,f +33446,100%,Philippines,1.0,t +14578,,Bosnia and Herzegovina,1.0,f +1647,,,1.0,f +34720,,,1.0,t +43090,,,1.0,t +19248,25%,Maldives,3.0,f +9777,,Isle of Man,1.0,f +38395,,,1.0,t +25315,100%,Uganda,2.0,t +30916,100%,Tunisia,3.0,f +64,100%,Marshall Islands,1.0,f +29084,100%,Mexico,2.0,t +18773,,,1.0,f +8665,100%,,1.0,f +47841,,Chad,1.0,t +37869,100%,,2.0,f +343,100%,Brazil,2.0,t +15220,,,1.0,t +17322,95%,,30.0,f +17523,100%,,1.0,f +1046,,Gambia,1.0,f +20254,,Marshall Islands,1.0,f +13854,100%,Portugal,1.0,f +35367,100%,Malta,1.0,t +22088,,China,1.0,f +6518,100%,Uzbekistan,1.0,f +20052,,Uzbekistan,1.0,f +28197,,,1.0,f +9905,,Zimbabwe,1.0,f +43928,,,1.0,f +43160,,Australia,1.0,t +28728,,,1.0,f +3480,100%,United Kingdom,2.0,f +36707,,Cocos (Keeling) Islands,1.0,t +8942,,,3.0,f +3436,100%,,2.0,f +8359,,Anguilla,1.0,f +45174,,,1.0,f +7933,,,1.0,t +6793,,Ecuador,1.0,t +24681,,Russian Federation,1.0,t +39563,100%,,1.0,f +10052,100%,Niue,1.0,t +10835,,Niger,1.0,f +2748,100%,Nicaragua,2.0,f +20647,100%,,1.0,f +16747,100%,,1.0,t +5358,,United Kingdom,1.0,t +48742,100%,Guinea,2.0,f +37820,,,1.0,t +21291,,Bosnia and Herzegovina,1.0,t +29104,,El Salvador,1.0,t +45010,,Russian Federation,1.0,t +4209,,,1.0,t +46852,100%,,1.0,t +44817,100%,Chile,1.0,t +46856,,Greenland,2.0,f +10677,100%,Venezuela,1.0,f +784,,Marshall Islands,1.0,f +14811,,,1.0,f +34037,0%,Togo,2.0,f +26805,,,1.0,f +39355,80%,Zimbabwe,2.0,t +47504,,France,1.0,f +10296,,,1.0,f +4566,100%,French Guiana,1.0,f +1024,100%,Nicaragua,2.0,f +2818,,,1.0,t +11882,,Sao Tome and Principe,2.0,t +3147,,Nicaragua,1.0,f +2479,100%,Isle of Man,1.0,f +18999,100%,Chile,2.0,f +29359,100%,Denmark,1.0,f +9605,,Turkmenistan,1.0,f +24233,,,1.0,f +29181,,Niue,1.0,f +49669,,,1.0,t +1212,100%,Tonga,2.0,f +4709,,Nicaragua,1.0,f +3575,,,1.0,f +10312,,,1.0,f +32130,,Cuba,2.0,t +5757,100%,Portugal,2.0,f +45609,100%,,1.0,t +40316,,Malta,1.0,f +26271,,,1.0,t +26701,100%,,2.0,f +46011,,Gambia,1.0,t +46749,100%,,2.0,f +42218,,,1.0,f +44972,,Micronesia,2.0,f +8846,,Russian Federation,1.0,t +2103,100%,,2.0,t +49877,,Nicaragua,1.0,f +23172,100%,,1.0,t +15717,,United Kingdom,1.0,f +359,,Gambia,1.0,t +47520,67%,,1.0,f +15695,100%,Malawi,2.0,t +34046,90%,Guinea,1.0,f +42311,,Lebanon,1.0,t +16970,100%,,1.0,f +16905,100%,Reunion,3.0,t +44571,,,1.0,f +24304,100%,Micronesia,1.0,t +22616,,Cocos (Keeling) Islands,1.0,f +23567,100%,Guinea,2.0,t +42273,100%,,1.0,f +31859,100%,Maldives,2.0,f +30939,,,1.0,f +2126,,Venezuela,1.0,f +4992,,Kiribati,5.0,t +4853,71%,,1.0,t +11451,,Peru,1.0,t +33449,,,1.0,f +44418,,Russian Federation,1.0,f +27258,100%,,1.0,f +6900,70%,,1.0,f +254,,,1.0,f +3360,100%,United Kingdom,1.0,f +8549,100%,Lebanon,1.0,t +35852,100%,Russian Federation,1.0,f +5171,100%,Anguilla,2.0,f +9827,,Somalia,1.0,t +47745,,,1.0,t +27290,100%,Brazil,3.0,f +45269,,Nauru,1.0,t +15860,100%,Maldives,11.0,f +49837,,,1.0,f +16769,80%,Isle of Man,2.0,f +43836,,,1.0,f +49695,,Nicaragua,1.0,f +17542,,,1.0,f +8309,100%,Brunei Darussalam,1.0,t +36432,,Kenya,1.0,t +43062,100%,,2.0,t +36495,50%,Denmark,1.0,f +26411,100%,Niue,1.0,t +31793,100%,,1.0,t +6600,80%,,1.0,f +14608,,,1.0,t +41224,100%,Mauritania,3.0,t +34343,100%,Marshall Islands,1.0,t +46754,,Montserrat,1.0,f +23243,,Lebanon,1.0,t +16304,100%,Guernsey,1.0,f +44286,100%,,1.0,f +11933,100%,,2.0,f +5103,100%,,1.0,f +36235,,Uzbekistan,1.0,f +15654,,French Guiana,1.0,t +4906,100%,Senegal,2.0,t +35718,,,1.0,t +29060,,,2.0,f +2246,0%,,1.0,f +9264,88%,Kenya,2.0,t +45084,,Gambia,1.0,t +24292,,Lebanon,1.0,f +2359,100%,,1.0,f +34054,100%,,4.0,f +41858,,,1.0,f +36100,100%,Rwanda,17.0,t +35650,,,1.0,f +41088,,,1.0,f +6258,,Sao Tome and Principe,1.0,f +47990,88%,Venezuela,4.0,f +17559,100%,Uruguay,1.0,f +30510,,Russian Federation,1.0,f +31856,,,1.0,t +18758,,Russian Federation,1.0,t +28426,100%,,1.0,f +20359,,,1.0,f +37368,100%,Niger,1.0,f +2809,,,1.0,t +44441,100%,United Kingdom,1.0,t +27575,,,1.0,f +2895,100%,France,1.0,f +21548,43%,Brazil,1.0,f +45748,0%,,1.0,f +49858,100%,Estonia,1.0,f +46188,100%,,1.0,f +362,100%,,1.0,f +18180,,,1.0,t +37833,,China,1.0,t +31753,,,1.0,t +43455,,Lebanon,1.0,f +38873,100%,Mauritania,4.0,f +5211,,Sao Tome and Principe,1.0,f +18763,,Bosnia and Herzegovina,1.0,f +10657,,,1.0,t +7169,,,2.0,f +17467,100%,Micronesia,1.0,f +20251,100%,Jersey,1.0,t +14892,100%,Turkmenistan,1.0,t +3858,,,1.0,t +33640,100%,French Polynesia,1.0,f +25840,,,1.0,f +25748,,Mauritania,1.0,t +44338,100%,Peru,2.0,t +415,100%,Zimbabwe,1.0,f +16623,80%,Denmark,1.0,f +13981,100%,Zimbabwe,1.0,f +20815,0%,,1.0,t +38971,,,1.0,f +20630,,Faroe Islands,1.0,f +38271,100%,,1.0,t +45416,,,1.0,f +46339,100%,Uzbekistan,1.0,t +4234,100%,Malawi,1.0,t +1941,100%,El Salvador,1.0,t +20809,90%,,1.0,f +14170,,,1.0,f +20151,,Marshall Islands,1.0,t +17608,0%,,1.0,f +49473,,,1.0,f +1969,,Micronesia,1.0,f +45465,100%,,1.0,f +49714,100%,Montserrat,1.0,t +15933,,,1.0,f +48626,,Turks and Caicos Islands,3.0,f +39361,,Bosnia and Herzegovina,1.0,f +27674,,Wallis and Futuna,1.0,t +45973,,Guernsey,1.0,f +10129,,,1.0,t +6208,73%,,1.0,f +19744,100%,,1.0,f +35292,100%,,1.0,f +18455,92%,,1.0,f +29241,100%,Lebanon,1.0,f +48606,100%,Russian Federation,1.0,t +16299,,Chad,1.0,f +20544,,,1.0,t +40239,,Lebanon,1.0,t +43350,100%,Isle of Man,2.0,t +42391,,Gambia,1.0,f +12772,100%,Niger,1.0,f +29680,88%,Faroe Islands,2.0,f +23070,,,2.0,f +42880,,Gambia,2.0,f +16134,,Andorra,1.0,f +12677,,,1.0,f +31070,,Chile,1.0,f +13653,,Isle of Man,3.0,t +19453,100%,,1.0,f +42299,100%,Marshall Islands,1.0,f +38347,100%,El Salvador,1.0,f +46598,0%,Zimbabwe,1.0,f +44832,,Chad,1.0,f +37341,,Pakistan,3.0,f +23790,100%,Isle of Man,1.0,t +17495,,,1.0,f +9884,100%,,1.0,t +19704,,Gambia,1.0,f +47914,80%,Philippines,1.0,t +48425,100%,,3.0,f +19228,100%,Svalbard & Jan Mayen Islands,8.0,f +35207,,Anguilla,1.0,t +38189,,Marshall Islands,1.0,f +29609,100%,Niue,1.0,t +2874,,,1.0,f +40275,,Micronesia,2.0,f +32519,0%,France,1.0,t +19715,,,1.0,f +30728,,,1.0,t +27239,100%,,1.0,f +15984,,Micronesia,1.0,f +10660,,,2.0,t +16983,,,1.0,f +21682,100%,United Kingdom,2.0,f +10103,100%,Zimbabwe,1.0,f +47366,,,1.0,t +46438,,Svalbard & Jan Mayen Islands,1.0,f +1956,100%,,1.0,t +45220,,Bosnia and Herzegovina,1.0,t +32967,,,1.0,f +6140,100%,Marshall Islands,3.0,f +25338,0%,,1.0,f +44717,100%,Monaco,2.0,t +32416,100%,,1.0,t +2201,,United Kingdom,1.0,f +37681,100%,,1.0,t +19631,100%,,1.0,f +43155,,Jersey,1.0,f +11707,,,1.0,f +30095,,,1.0,t +20574,100%,Malawi,1.0,t +6230,0%,Marshall Islands,1.0,f +40253,,Sao Tome and Principe,1.0,f +43200,,,1.0,t +26790,100%,,1.0,f +36024,,,1.0,t +9589,100%,Faroe Islands,1.0,f +35095,,Isle of Man,1.0,t +28858,,Vanuatu,1.0,f +6923,100%,Anguilla,1.0,t +37031,,Kenya,1.0,f +31047,100%,Turkmenistan,1.0,f +1862,33%,Micronesia,1.0,f +10896,100%,Bouvet Island (Bouvetoya),1.0,f +3174,100%,,1.0,t +21053,90%,Senegal,1.0,f +32873,,,1.0,t +45066,100%,Pakistan,1.0,f +9476,,French Guiana,1.0,t +25583,,Jersey,1.0,t +39546,100%,,1.0,f +34368,,Croatia,1.0,t +12497,100%,,2.0,f +30284,100%,Montserrat,1.0,f +38576,,,1.0,f +3083,100%,,1.0,f +28293,,Sao Tome and Principe,1.0,t +7737,,,1.0,f +33631,,Chad,1.0,t +35493,100%,French Guiana,1.0,t +36686,,Lebanon,1.0,f +9873,100%,Kenya,1.0,f +49426,,,1.0,t +22156,,Svalbard & Jan Mayen Islands,1.0,f +30414,,,2.0,f +15214,,Maldives,1.0,t +14708,,Nicaragua,1.0,f +31801,50%,Tonga,2.0,f +15568,,,1.0,t +17847,100%,Anguilla,1.0,f +7973,,Niue,1.0,t +4706,,Guinea,1.0,f +34506,,,1.0,f +40977,100%,Reunion,7.0,f +48765,100%,Kenya,1.0,t +44066,,Philippines,1.0,t +10841,80%,,2.0,f +18017,,,1.0,t +3001,,Zimbabwe,1.0,f +141,,Bosnia and Herzegovina,1.0,f +39265,,Sao Tome and Principe,2.0,f +32578,74%,Zimbabwe,2.0,t +48529,100%,Tonga,1.0,f +26226,,,1.0,f +15185,100%,Uzbekistan,1.0,f +49807,100%,,1.0,t +27159,,Isle of Man,1.0,t +8943,,,1.0,f +17502,80%,Netherlands,1.0,f +18304,,Estonia,1.0,t +20650,100%,Uzbekistan,3.0,t +2198,100%,Marshall Islands,1.0,t +32718,90%,,1.0,f +1835,100%,Ecuador,8.0,f +46956,63%,Jersey,2.0,t +12133,100%,Mexico,4.0,f +34795,,Indonesia,1.0,f +34184,,,3.0,f +41238,100%,Kiribati,1.0,t +27601,100%,,1.0,f +35510,100%,,1.0,f +19739,100%,Holy See (Vatican City State),1.0,t +20633,,,1.0,f +47439,100%,,2.0,t +3367,,Faroe Islands,1.0,f +31495,,Uzbekistan,1.0,f +13184,100%,Vietnam,7.0,f +28206,,Cocos (Keeling) Islands,1.0,f +48549,100%,France,2.0,f +30409,100%,,3.0,t +32533,100%,,1.0,f +22982,100%,,1.0,t +18714,67%,French Guiana,1.0,f +5122,,El Salvador,1.0,f +6252,100%,,1.0,f +49690,,China,1.0,f +2430,100%,,1.0,f +35659,,,1.0,f +34453,,,1.0,f +32735,100%,,2.0,f +5048,100%,Niue,2.0,t +6711,100%,Spain,3.0,f +3085,,,1.0,t +17186,,China,2.0,f +37238,100%,,1.0,f +22763,,Guinea,1.0,t +37020,,Chile,1.0,t +49016,,,2.0,f +6954,,Anguilla,2.0,f +23308,100%,Tonga,2.0,t +41867,100%,Monaco,1.0,t +4044,,Malta,1.0,f +17352,100%,,1.0,t +10691,,Chile,1.0,f +28448,,Brazil,1.0,t +16904,,Turks and Caicos Islands,1.0,f +42197,100%,,1.0,t +9468,,,1.0,t +31534,100%,,1.0,f +18031,100%,Reunion,4.0,f +25995,,Gambia,1.0,f +39010,,Russian Federation,1.0,t +23867,,,1.0,t +40478,83%,,2.0,t +18264,,France,1.0,t +23773,100%,El Salvador,1.0,t +19006,,Guinea,1.0,f +26828,,Russian Federation,1.0,f +46815,,Tanzania,1.0,t +33778,,China,1.0,f +20204,,Argentina,2.0,f +47945,,,2.0,f +21635,100%,,1.0,t +22326,,,1.0,t +35544,100%,Kenya,1.0,t +21089,,Holy See (Vatican City State),1.0,t +49479,,Russian Federation,1.0,t +38883,80%,,1.0,f +4852,,Rwanda,1.0,f +22127,100%,United Kingdom,1.0,f +39868,,Chile,1.0,f +41451,,Isle of Man,1.0,t +41207,100%,Svalbard & Jan Mayen Islands,1.0,t +15006,,,1.0,f +28102,,,1.0,f +8323,,Sao Tome and Principe,1.0,f +26650,,,2.0,t +36943,,Zimbabwe,2.0,t +18272,90%,Vietnam,3.0,f +34743,,French Guiana,1.0,f +28259,,Niue,1.0,t +47065,,Cocos (Keeling) Islands,1.0,f +34936,,Maldives,1.0,t +42058,,Nicaragua,1.0,f +44474,,,2.0,t +34382,100%,Micronesia,2.0,f +20044,,,1.0,f +14699,67%,Turkmenistan,1.0,t +37544,100%,Malta,2.0,f +20124,100%,France,2.0,t +15311,100%,Holy See (Vatican City State),1.0,f +27026,,,1.0,t +37988,,Puerto Rico,1.0,f +10228,,Russian Federation,1.0,t +4012,,French Polynesia,1.0,f +4923,,,1.0,t +22855,100%,French Guiana,1.0,f +25701,50%,,1.0,f +24714,,Cape Verde,1.0,f +3423,,Nauru,3.0,f +14030,,,1.0,f +24625,100%,Micronesia,1.0,t +49162,,Niue,1.0,t +23581,,Russian Federation,1.0,t +11047,96%,Senegal,3.0,t +56,100%,Kiribati,2.0,t +23712,,Russian Federation,1.0,f +14626,,Niue,1.0,f +40725,,Gambia,1.0,f +24439,,Guinea,1.0,f +39812,100%,Nicaragua,6.0,t +10305,,Somalia,3.0,t +12403,92%,Vietnam,3.0,t +4168,,Monaco,2.0,t +11525,90%,Australia,1.0,t +38098,100%,Zimbabwe,7.0,t +39825,,,1.0,f +16115,,Peru,1.0,t +1378,,Guinea,1.0,t +31770,,,2.0,f +46656,50%,France,2.0,f +19777,100%,French Guiana,1.0,f +21247,100%,Guernsey,1.0,f +45191,100%,Montserrat,3.0,t +24419,,Malawi,2.0,f +46695,,Afghanistan,1.0,f +3835,,,1.0,f +35685,100%,Niue,1.0,f +38705,,Guinea,1.0,t +33494,100%,Papua New Guinea,1.0,f +38698,100%,France,2.0,t +26875,100%,Tanzania,1.0,f +8582,0%,,1.0,t +20727,100%,Russian Federation,4.0,t +49133,100%,Reunion,3.0,t +1711,100%,Jersey,1.0,f +23615,100%,Marshall Islands,1.0,t +8515,,Isle of Man,1.0,f +20371,,,1.0,f +29780,,,1.0,t +1169,,Guinea,3.0,f +45067,75%,,3.0,t +18574,,,1.0,f +22192,,,1.0,t +12321,100%,,1.0,f +38277,100%,Guinea,1.0,f +20208,,Croatia,1.0,t +20385,,,1.0,f +10442,,Isle of Man,1.0,f +18619,100%,Slovakia (Slovak Republic),1.0,f +26778,100%,Kiribati,2.0,f +7436,,,1.0,t +19682,100%,Faroe Islands,15.0,t +38541,,,1.0,f +776,100%,,1.0,t +9131,100%,Gambia,5.0,f +34043,,Venezuela,1.0,t +44042,100%,,1.0,t +9915,,Brunei Darussalam,2.0,f +3786,100%,,1.0,f +36273,,,2.0,f +38301,90%,Spain,2.0,f +38406,96%,Russian Federation,6.0,t +1576,50%,,1.0,f +41637,,Niue,1.0,f +38013,100%,,1.0,f +26878,80%,Monaco,2.0,t +37477,,Costa Rica,1.0,f +44670,,Monaco,1.0,f +25001,100%,Saint Helena,1.0,f +467,50%,,1.0,f +40234,100%,Isle of Man,1.0,f +32627,100%,,1.0,f +15362,,Guinea,1.0,t +11798,,French Guiana,1.0,t +1848,,Kenya,1.0,t +42811,100%,Isle of Man,1.0,f +23027,,Anguilla,1.0,f +19154,100%,France,2.0,t +23275,,Micronesia,5.0,t +34136,,Chad,1.0,t +50003,,Zimbabwe,1.0,t +46636,,,1.0,t +15868,,Zimbabwe,1.0,t +1574,,Turks and Caicos Islands,1.0,t +12618,,Isle of Man,1.0,f +2667,100%,Niue,3.0,f +14641,100%,,1.0,t +34130,,,1.0,f +19871,,China,1.0,t +36510,,Tonga,1.0,t +27909,,Cocos (Keeling) Islands,1.0,f +23696,100%,Kiribati,1.0,t +27265,100%,Togo,2.0,t +48766,100%,Uzbekistan,1.0,f +2322,100%,Denmark,2.0,f +27724,,Lebanon,1.0,t +40752,100%,Tunisia,5.0,t +13114,100%,Indonesia,1.0,f +6496,83%,Cuba,2.0,t +47527,100%,Tonga,1.0,f +27456,,,1.0,t +17327,,Russian Federation,2.0,f +7687,,Brunei Darussalam,1.0,f +5158,100%,,1.0,f +43640,100%,Turks and Caicos Islands,1.0,f +34435,,,1.0,t +44669,33%,France,1.0,f +44499,100%,,1.0,f +24916,100%,,1.0,f +19263,,Guinea,1.0,t +38611,100%,,1.0,t +43273,,Uzbekistan,1.0,f +8893,,Lebanon,1.0,f +22424,,Isle of Man,1.0,t +40961,100%,Venezuela,1.0,f +39330,,Jersey,1.0,f +681,,Mauritania,1.0,t +34096,100%,Andorra,10.0,f +48295,100%,Jersey,1.0,t +31481,,France,1.0,f +49504,100%,Guinea,2.0,f +10060,,Sao Tome and Principe,2.0,f +10650,,,1.0,f +44823,,Micronesia,2.0,f +33634,,Guinea,1.0,f +7802,100%,Anguilla,1.0,t +49862,,Zimbabwe,1.0,f +49280,,Turkmenistan,1.0,f +30695,,Guinea,1.0,f +23281,100%,Isle of Man,1.0,t +1992,,,1.0,f +870,,Somalia,1.0,f +20327,100%,Chad,1.0,t +31706,,Niue,1.0,f +8583,90%,Venezuela,1.0,f +21441,100%,Tanzania,1.0,t +41214,,Guinea,1.0,t +28162,,Ecuador,1.0,f +16562,,Tunisia,1.0,t +40831,100%,Croatia,1.0,f +29581,100%,Nicaragua,1.0,f +42989,100%,,2.0,f +21795,,Bosnia and Herzegovina,1.0,t +42438,100%,,1.0,t +31146,,,1.0,t +27003,100%,,1.0,t +21085,,Russian Federation,1.0,t +38034,80%,,1.0,f +33202,100%,,2.0,t +11749,100%,Isle of Man,1.0,t +17295,,Jersey,1.0,f +16181,100%,Tanzania,2.0,f +30683,100%,Marshall Islands,1.0,f +24939,0%,,1.0,f +4302,,Lebanon,2.0,t +10018,,,1.0,t +22118,100%,Chad,1.0,t +3165,100%,Gibraltar,1.0,f +46324,,Cocos (Keeling) Islands,1.0,f +10635,100%,Peru,1.0,t +10265,100%,Micronesia,3.0,f +47247,,Somalia,1.0,f +31719,100%,Pakistan,1.0,t +40964,,,1.0,t +38711,,Russian Federation,1.0,f +36931,100%,Russian Federation,2.0,f +27314,,,1.0,f +14110,,Tanzania,1.0,f +5521,100%,,1.0,f +14321,,Russian Federation,2.0,t +25755,,El Salvador,1.0,t +13704,,,1.0,t +30785,100%,Jersey,1.0,t +14035,94%,Greenland,1.0,f +12391,,Zimbabwe,1.0,f +49915,100%,Tunisia,1.0,t +39055,,Micronesia,1.0,t +22513,90%,Jersey,4.0,t +3532,100%,Congo,1.0,f +36733,100%,Tanzania,1.0,t +39982,,Estonia,1.0,f +9583,,Zimbabwe,1.0,f +34653,100%,Turkmenistan,1.0,t +29932,100%,France,2.0,t +45201,,,1.0,f +37298,,Montserrat,1.0,t +3796,,Togo,1.0,f +31005,,,1.0,f +26129,,Russian Federation,1.0,f +43100,100%,Rwanda,1.0,f +18715,100%,,1.0,t +12901,,Lebanon,1.0,f +928,,,1.0,f +13299,,Lithuania,1.0,t +46644,100%,Finland,1.0,f +48448,,Reunion,1.0,f +12823,100%,Niue,2.0,f +47938,100%,Niue,4.0,f +6354,,,1.0,f +41153,100%,Guinea,9.0,f +43274,,France,1.0,t +40263,,Nauru,3.0,f +38763,,Bouvet Island (Bouvetoya),1.0,f +522,,,1.0,t +18362,100%,Uganda,5.0,f +32530,100%,Chad,1.0,t +32324,100%,Zimbabwe,1.0,f +18374,100%,,3.0,f +25824,100%,Croatia,1.0,t +21996,,,1.0,f +36874,80%,,2.0,f +10571,100%,Niue,1.0,f +20686,100%,Guernsey,1.0,f +47257,,,1.0,f +18528,96%,Anguilla,1.0,f +1654,100%,,1.0,t +28946,100%,Vanuatu,1.0,t +7970,90%,Isle of Man,3.0,t +34376,,Nauru,1.0,f +9551,,Denmark,1.0,f +44777,100%,Russian Federation,1.0,t +28249,,Bosnia and Herzegovina,1.0,f +43973,,,1.0,f +49692,,,1.0,t +19913,100%,,1.0,t +22495,,Nicaragua,1.0,t +16107,100%,Brazil,1.0,t +47510,,Russian Federation,1.0,f +2935,100%,Isle of Man,3.0,t +17472,100%,Holy See (Vatican City State),2.0,t +32155,,Uzbekistan,2.0,t +46798,,Micronesia,2.0,f +36816,100%,Svalbard & Jan Mayen Islands,1.0,f +31090,75%,,1.0,t +12350,100%,Niger,2.0,t +21735,50%,Isle of Man,1.0,t +46780,,,1.0,t +3649,100%,Russian Federation,1.0,f +28981,,Turkmenistan,1.0,t +34925,100%,Maldives,1.0,f +20786,,Mauritania,1.0,t +30757,90%,Croatia,2.0,f +38982,,Cocos (Keeling) Islands,1.0,t +33034,,Vietnam,1.0,f +23329,100%,Niue,1.0,f +29523,100%,,1.0,f +16238,,,1.0,t +47817,100%,Saint Helena,2.0,f +26114,100%,,1.0,t +11403,80%,Barbados,3.0,f +18611,,Chad,1.0,t +37392,100%,Micronesia,7.0,t +39973,100%,Micronesia,1.0,f +14984,100%,,2.0,f +2571,60%,,1.0,f +15923,,Denmark,1.0,f +32208,,Guinea,1.0,f +22315,,Malawi,1.0,f +12032,100%,Mauritania,2.0,t +15168,100%,Venezuela,2.0,f +38362,,,1.0,f +42589,100%,Turkmenistan,1.0,f +22457,80%,Venezuela,1.0,f +8734,,Estonia,1.0,f +15436,,Tonga,1.0,t +1194,100%,Anguilla,1.0,f +4057,100%,Rwanda,16.0,f +3583,,Niue,1.0,f +47782,,,1.0,f +35078,100%,Venezuela,1.0,f +32358,,France,1.0,t +2716,,French Guiana,1.0,f +4789,,Indonesia,1.0,t +25239,100%,Cape Verde,1.0,f +17174,100%,Mauritania,1.0,t +35102,,Maldives,1.0,t +25997,,Sao Tome and Principe,1.0,f +27108,,France,1.0,f +21161,100%,Marshall Islands,1.0,f +2553,100%,Turks and Caicos Islands,3.0,f +24402,,Estonia,1.0,f +12499,,,1.0,t +8866,90%,Tonga,2.0,t +23472,80%,Gibraltar,1.0,f +19642,,,1.0,t +29908,,United Kingdom,1.0,f +7967,100%,Venezuela,1.0,f +73,,Svalbard & Jan Mayen Islands,1.0,t +43947,100%,Greenland,1.0,f +49632,10%,Reunion,1.0,f +24059,,,1.0,t +10151,,Uganda,1.0,t +8511,,,1.0,t +40765,100%,,1.0,f +41511,100%,,1.0,f +23943,100%,Nicaragua,2.0,t +19310,100%,Indonesia,1.0,t +31276,,,1.0,t +50004,100%,,1.0,t +47777,0%,Isle of Man,1.0,t +20714,100%,Isle of Man,1.0,t +45333,,United Kingdom,1.0,f +28509,,Zimbabwe,1.0,f +43093,,Niue,1.0,f +28134,,Guernsey,1.0,t +5004,,Micronesia,1.0,f +45078,100%,Barbados,1.0,f +23372,100%,Niger,5.0,t +32042,,Tonga,1.0,t +14346,,,3.0,t +40464,100%,,1.0,f +38438,,Venezuela,1.0,f +29424,100%,Sao Tome and Principe,1.0,f +40338,100%,Niue,1.0,f +27587,75%,,1.0,f +14494,100%,Rwanda,3.0,f +14529,,Peru,2.0,t +37910,,,2.0,f +17416,88%,Turkmenistan,1.0,f +44359,,Canada,1.0,t +13723,100%,Gambia,4.0,t +42044,,Uruguay,1.0,f +40559,95%,Tonga,1.0,f +30145,,,3.0,t +16654,,,1.0,t +5892,,Turkmenistan,1.0,t +21252,,Guinea,1.0,t +28458,100%,Turkmenistan,6.0,f +6148,100%,,1.0,t +9594,,Nicaragua,1.0,t +20563,86%,,1.0,f +11256,100%,Kenya,1.0,f +28531,100%,Russian Federation,1.0,f +35079,100%,,1.0,t +46735,,Gibraltar,1.0,f +32445,,Papua New Guinea,1.0,t +48255,,Sao Tome and Principe,1.0,f +18499,100%,Saint Helena,5.0,f +40507,,Congo,2.0,f +1898,,,1.0,t +30761,,,2.0,t +39522,,,1.0,f +49814,,Anguilla,1.0,f +29640,,Uzbekistan,1.0,f +38992,100%,,1.0,f +18202,,Isle of Man,1.0,f +4578,,Russian Federation,1.0,t +39601,,Faroe Islands,1.0,f +17350,,Faroe Islands,1.0,f +32932,,Reunion,1.0,f +2039,,Jersey,1.0,t +33624,100%,,1.0,f +16147,,,1.0,f +2248,,,5.0,f +7519,,,1.0,f +36064,,Russian Federation,1.0,f +31956,100%,Uruguay,1.0,f +13290,,,1.0,f +2351,,,1.0,t +28658,,,1.0,f +24164,0%,,1.0,f +48530,100%,Malta,1.0,t +5938,100%,,3.0,f +33394,100%,Bosnia and Herzegovina,2.0,t +5936,,French Guiana,1.0,f +10224,,Nicaragua,2.0,t +9855,100%,,1.0,f +44334,100%,Lebanon,1.0,t +25628,100%,Estonia,1.0,f +5949,,,1.0,f +21438,100%,China,3.0,f +20881,,Rwanda,1.0,t +49809,,,1.0,f +48848,100%,Cocos (Keeling) Islands,1.0,f +20366,,,1.0,t +41505,,,1.0,t +7144,,Niger,1.0,t +3975,,Chad,1.0,f +40398,,,1.0,f +45147,,Rwanda,1.0,t +38668,100%,Lithuania,1.0,t +35754,0%,Lebanon,1.0,t +48113,100%,Marshall Islands,1.0,f +37802,,Micronesia,1.0,f +41115,,,1.0,t +15268,,Lebanon,1.0,t +45618,,French Polynesia,1.0,f +45956,100%,,1.0,f +5808,,,1.0,t +19378,,Zimbabwe,1.0,t +39129,67%,Mauritania,3.0,t +20178,,Zimbabwe,1.0,f +41539,,Puerto Rico,1.0,f +46058,100%,Turkmenistan,3.0,t +20754,,China,13.0,f +48837,100%,,1.0,f +5159,,Brazil,1.0,f +36292,100%,,1.0,f +18622,,Cocos (Keeling) Islands,1.0,t +13864,,,1.0,f +45931,,Niue,1.0,f +42508,,,1.0,t +23156,,,1.0,f +9672,,Bosnia and Herzegovina,3.0,t +2131,100%,,1.0,t +29056,,,2.0,f +49119,100%,Papua New Guinea,1.0,t +36758,89%,Russian Federation,2.0,f +10788,25%,Lebanon,2.0,f +584,,,1.0,f +20835,100%,Gibraltar,3.0,f +50051,100%,Netherlands,5.0,t +24723,,Uzbekistan,1.0,f +17142,100%,Anguilla,1.0,f +49256,,,1.0,f +39992,,Nicaragua,1.0,t +37125,100%,Rwanda,1.0,f +28771,,Greenland,1.0,f +35362,,Gambia,1.0,f +23780,,Faroe Islands,3.0,f +32889,,Rwanda,1.0,f +33004,100%,Russian Federation,2.0,t +32980,100%,Micronesia,1.0,f +39418,,Niue,1.0,t +37023,,Faroe Islands,1.0,t +7478,,,2.0,t +35442,,Russian Federation,14.0,f +42878,100%,Greenland,1.0,f +14419,92%,Cuba,2.0,t +29306,100%,,1.0,f +12080,,,1.0,t +1798,,Faroe Islands,2.0,t +23144,,Zimbabwe,1.0,f +43476,100%,Gibraltar,2.0,t +9008,,,3.0,t +30610,100%,Kiribati,1.0,t +37567,,Estonia,2.0,f +15100,100%,,1.0,t +49906,100%,,1.0,f +42433,,,1.0,t +42448,,Portugal,1.0,t +293,100%,Lebanon,1.0,f +28668,100%,Gibraltar,1.0,t +13907,,,1.0,f +32915,100%,Zimbabwe,1.0,t +3948,,Niue,1.0,f +25472,100%,,1.0,f +37362,100%,Niue,1.0,f +16859,100%,Slovakia (Slovak Republic),1.0,t +32263,100%,,1.0,t +43677,50%,,1.0,f +17004,100%,United Kingdom,1.0,f +35513,100%,United Kingdom,1.0,f +18851,100%,Russian Federation,1.0,t +38829,,Canada,1.0,f +23143,65%,Bouvet Island (Bouvetoya),14.0,f +24817,,Gambia,2.0,t +26695,,,1.0,f +21707,,Niue,1.0,f +24353,100%,Isle of Man,1.0,f +513,80%,Uganda,1.0,f +23801,,Wallis and Futuna,1.0,f +23877,100%,,1.0,f +25226,100%,,1.0,t +14031,,Sao Tome and Principe,1.0,f +12528,0%,Fiji,1.0,f +27792,100%,,2.0,f +45602,100%,,1.0,t +17628,,Chile,1.0,t +5227,100%,Gambia,1.0,f +3653,100%,,1.0,t +21922,75%,Sao Tome and Principe,1.0,t +23752,,Canada,1.0,f +8103,100%,Tonga,1.0,f +32101,100%,,1.0,f +42114,100%,Sao Tome and Principe,3.0,t +17341,70%,Malawi,2.0,f +39953,,Maldives,1.0,t +12953,100%,Guernsey,1.0,f +42408,,Afghanistan,1.0,t +9171,,Svalbard & Jan Mayen Islands,1.0,t +32211,,Anguilla,1.0,f +15771,100%,,3.0,t +16239,,Sao Tome and Principe,1.0,t +7381,100%,Micronesia,1.0,f +5409,100%,Russian Federation,1.0,f +14879,,,1.0,f +20856,,,1.0,t +5568,,Montserrat,1.0,t +44494,,Malta,2.0,f +35786,100%,Isle of Man,1.0,f +36113,,Tonga,1.0,t +7229,,,1.0,f +15180,,Djibouti,1.0,t +35307,100%,,1.0,f +25273,90%,,1.0,t +45322,100%,,1.0,f +29220,50%,Costa Rica,1.0,t +8939,,Isle of Man,1.0,t +45142,,,1.0,t +19678,90%,Cocos (Keeling) Islands,1.0,t +46082,100%,Isle of Man,1.0,t +18134,,Mauritania,1.0,f +24368,,,1.0,f +9362,100%,Turkmenistan,1.0,t +42357,,Micronesia,1.0,t +1264,90%,Denmark,1.0,f +39767,100%,,1.0,f +25005,100%,Papua New Guinea,1.0,t +133,0%,,1.0,f +23045,,,3.0,f +21439,,Faroe Islands,1.0,t +22362,,,1.0,f +4846,100%,Isle of Man,1.0,f +47869,100%,Gambia,1.0,f +13654,0%,Chad,1.0,f +21182,,Lebanon,1.0,f +24128,54%,Suriname,9.0,t +34062,,Uganda,5.0,t +17760,,Anguilla,2.0,f +19750,,Niger,1.0,f +15260,,Maldives,1.0,f +36250,,Tonga,1.0,t +9000,,Papua New Guinea,1.0,t +22675,100%,Indonesia,1.0,f +28432,,Isle of Man,1.0,f +6260,,,1.0,f +32176,,Bosnia and Herzegovina,1.0,f +2561,,Russian Federation,1.0,f +29787,,,1.0,f +49148,,French Guiana,1.0,f +38060,,Chad,1.0,t +13961,,,1.0,t +26864,,,1.0,f +23124,100%,,1.0,f +7197,100%,Niue,1.0,t +43081,100%,Estonia,1.0,f +6311,,Gambia,1.0,t +8775,,,1.0,t +5220,90%,Papua New Guinea,1.0,f +15454,100%,Denmark,1.0,f +17262,100%,Slovakia (Slovak Republic),1.0,f +6061,0%,Tonga,2.0,t +40910,100%,France,2.0,f +20485,100%,China,1.0,f +33801,,,1.0,f +29114,,Monaco,1.0,f +45475,100%,Montserrat,2.0,f +32055,29%,,1.0,f +40296,100%,Ukraine,1.0,f +21025,,,1.0,t +15987,67%,Costa Rica,1.0,f +35070,100%,,3.0,f +24950,,Denmark,1.0,t +485,80%,French Guiana,2.0,f +36004,33%,,2.0,t +5569,100%,Suriname,2.0,t +34122,0%,,1.0,f +17104,,Bosnia and Herzegovina,2.0,t +2368,78%,France,1.0,t +19958,100%,Congo,40.0,t +58,,,1.0,t +28794,100%,Denmark,1.0,f +28203,100%,,2.0,t +21422,,Gambia,1.0,f +22872,,,1.0,f +3230,100%,Turkmenistan,2.0,t +46881,100%,Andorra,1.0,f +5128,100%,Tunisia,2.0,t +41840,100%,Niue,1.0,f +10348,100%,Tunisia,1.0,f +16973,100%,Lithuania,1.0,f +27894,,Zimbabwe,1.0,f +23219,,,1.0,f +16284,,Lebanon,1.0,t +3840,100%,Estonia,1.0,t +16257,100%,Turkmenistan,1.0,t +29448,,Mauritania,1.0,t +6403,,,1.0,t +12459,,Rwanda,1.0,f +4723,100%,,8.0,f +5166,100%,Ecuador,16.0,t +39378,100%,Brazil,1.0,f +43002,100%,Gambia,1.0,f +42613,,Monaco,1.0,f +45991,100%,Sao Tome and Principe,1.0,f +16908,,Gibraltar,1.0,f +48541,,Russian Federation,2.0,t +35210,,China,1.0,t +41277,,Niue,1.0,f +25043,,,1.0,f +33538,100%,Isle of Man,1.0,f +37610,,,1.0,f +30354,,Gibraltar,1.0,f +36002,100%,Niue,1.0,f +12056,,Turkmenistan,1.0,t +32641,,Tonga,1.0,f +31244,57%,Niger,4.0,t +21599,,Brazil,1.0,t +41944,,,1.0,t +47033,100%,Estonia,1.0,f +33305,,Malta,1.0,t +40953,100%,Cocos (Keeling) Islands,1.0,f +8678,100%,Saint Helena,2.0,t +15955,88%,Malta,4.0,t +51,,Montserrat,1.0,t +35585,,Bouvet Island (Bouvetoya),2.0,f +26274,100%,Niue,1.0,f +36066,96%,Isle of Man,2.0,t +7724,100%,Uzbekistan,1.0,f +14119,,Vanuatu,1.0,f +21080,100%,Mexico,1.0,f +46984,,Ecuador,1.0,f +26735,100%,,1.0,f +12281,100%,Malta,1.0,f +11222,100%,,1.0,f +48186,100%,,1.0,t +21949,100%,Monaco,1.0,t +26913,,Niue,1.0,t +49177,,Anguilla,2.0,t +1618,100%,,1.0,t +6957,,Rwanda,1.0,t +7554,100%,,1105.0,f +34243,95%,Uzbekistan,1.0,f +12502,89%,Denmark,1.0,f +20213,,Russian Federation,1.0,f +2235,100%,Guinea,1.0,f +2353,100%,Senegal,2.0,t +49772,100%,Jersey,1.0,f +16548,90%,,2.0,f diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/reviews.csv b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/reviews.csv new file mode 100644 index 00000000..b472d0cb --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/reviews.csv @@ -0,0 +1,15532 @@ +shuttle_id,review_scores_rating,review_scores_comfort,review_scores_amenities,review_scores_trip,review_scores_crew,review_scores_location,review_scores_price,number_of_reviews,reviews_per_month +45163,91.0,10.0,9.0,9.0,9.0,9.0,9.0,26,0.77 +49438,96.0,10.0,10.0,10.0,10.0,10.0,9.0,61,0.62 +10750,97.0,10.0,10.0,10.0,10.0,10.0,10.0,467,4.66 +4146,95.0,10.0,10.0,10.0,10.0,9.0,9.0,318,3.22 +5067,97.0,10.0,9.0,10.0,10.0,9.0,10.0,22,0.29 +14891,93.0,9.0,8.0,10.0,9.0,9.0,9.0,15,0.19 +5689,92.0,9.0,10.0,9.0,10.0,10.0,9.0,75,0.79 +75321,97.0,10.0,10.0,10.0,10.0,9.0,10.0,450,4.5 +27341,,,,,,,,0, +25733,94.0,10.0,9.0,10.0,10.0,9.0,9.0,168,2.55 +38733,88.0,10.0,8.0,9.0,9.0,9.0,9.0,38,0.41 +61198,96.0,9.0,9.0,10.0,10.0,10.0,9.0,95,0.95 +9858,,,,,,,,0, +18221,95.0,9.0,10.0,10.0,10.0,10.0,9.0,184,1.9 +18590,95.0,10.0,9.0,10.0,10.0,9.0,10.0,172,1.91 +19813,72.0,7.0,8.0,8.0,8.0,9.0,8.0,10,0.15 +74856,95.0,9.0,10.0,10.0,10.0,9.0,9.0,176,1.95 +76722,98.0,10.0,10.0,10.0,10.0,10.0,10.0,30,0.41 +29755,93.0,9.0,10.0,10.0,10.0,9.0,9.0,23,0.27 +12358,95.0,9.0,9.0,10.0,10.0,10.0,9.0,23,0.28 +16167,97.0,10.0,10.0,10.0,10.0,10.0,9.0,165,1.82 +259,94.0,10.0,10.0,10.0,10.0,9.0,9.0,182,1.97 +71339,96.0,10.0,9.0,10.0,10.0,9.0,10.0,55,0.59 +48376,,,,,,,,0, +25335,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +56080,100.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.07 +66614,93.0,9.0,9.0,9.0,9.0,10.0,9.0,15,0.18 +51751,97.0,10.0,9.0,10.0,10.0,9.0,9.0,41,0.66 +16137,96.0,9.0,9.0,10.0,10.0,9.0,9.0,65,0.71 +54844,94.0,10.0,9.0,10.0,10.0,9.0,9.0,74,1.05 +58486,60.0,9.0,8.0,8.0,8.0,9.0,7.0,3,0.03 +14259,91.0,9.0,9.0,10.0,10.0,10.0,9.0,202,2.2 +41603,93.0,9.0,10.0,10.0,10.0,9.0,9.0,29,0.33 +258,95.0,9.0,9.0,10.0,10.0,9.0,10.0,87,0.97 +5637,87.0,9.0,9.0,9.0,9.0,9.0,9.0,25,0.34 +23951,97.0,10.0,10.0,10.0,10.0,9.0,10.0,34,0.93 +2990,95.0,9.0,9.0,10.0,10.0,10.0,9.0,25,0.28 +7653,94.0,10.0,10.0,10.0,10.0,9.0,9.0,70,0.8 +74286,95.0,10.0,10.0,10.0,10.0,9.0,9.0,29,0.35 +40515,97.0,10.0,9.0,9.0,10.0,10.0,9.0,64,0.73 +45692,91.0,9.0,10.0,10.0,10.0,8.0,9.0,15,0.28 +15000,97.0,10.0,10.0,10.0,10.0,9.0,10.0,141,1.6 +48747,97.0,10.0,9.0,10.0,10.0,9.0,10.0,21,0.3 +43966,85.0,9.0,9.0,10.0,10.0,9.0,9.0,4,0.13 +18155,99.0,10.0,10.0,10.0,10.0,10.0,10.0,134,1.51 +58876,70.0,9.0,9.0,10.0,10.0,9.0,7.0,5,0.06 +24706,100.0,10.0,10.0,6.0,10.0,8.0,10.0,1,0.01 +70604,96.0,10.0,9.0,9.0,10.0,9.0,9.0,18,0.21 +26471,,,,,,,,0, +49800,95.0,10.0,10.0,10.0,10.0,10.0,10.0,142,1.61 +22234,73.0,7.0,9.0,9.0,9.0,9.0,8.0,7,0.1 +16340,94.0,9.0,9.0,10.0,10.0,10.0,9.0,42,0.49 +41203,83.0,8.0,8.0,8.0,8.0,9.0,9.0,20,0.29 +17610,94.0,9.0,10.0,10.0,10.0,9.0,9.0,63,0.73 +6684,87.0,9.0,8.0,9.0,10.0,9.0,9.0,30,0.35 +31569,88.0,9.0,9.0,9.0,9.0,9.0,9.0,193,2.27 +76975,94.0,10.0,10.0,10.0,10.0,10.0,10.0,49,0.68 +17713,,,,,,,,0, +61591,90.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.05 +13839,100.0,10.0,10.0,10.0,8.0,10.0,10.0,2,0.02 +32376,92.0,10.0,10.0,9.0,9.0,10.0,9.0,431,5.0 +35048,93.0,10.0,9.0,9.0,10.0,10.0,9.0,46,0.53 +77029,94.0,10.0,9.0,10.0,10.0,10.0,9.0,108,1.34 +6062,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.01 +9155,94.0,9.0,9.0,10.0,10.0,9.0,9.0,107,1.33 +57334,96.0,10.0,9.0,10.0,10.0,10.0,10.0,28,0.33 +2266,98.0,10.0,10.0,10.0,10.0,9.0,10.0,21,0.25 +62819,,,,,,,,0, +24650,91.0,9.0,9.0,9.0,9.0,8.0,9.0,29,0.35 +40761,82.0,9.0,8.0,9.0,10.0,9.0,8.0,134,1.56 +62092,,,,,,,,0, +8691,89.0,9.0,9.0,9.0,9.0,9.0,9.0,38,0.47 +53116,,,,,,,,0, +50190,,,,,,,,0, +48391,88.0,9.0,9.0,9.0,9.0,9.0,9.0,120,1.45 +56396,98.0,10.0,10.0,10.0,10.0,10.0,10.0,471,5.49 +30465,87.0,8.0,7.0,8.0,9.0,9.0,9.0,12,0.14 +55579,93.0,9.0,10.0,9.0,10.0,10.0,9.0,390,4.58 +28853,97.0,10.0,10.0,10.0,10.0,9.0,10.0,43,0.5 +68424,95.0,10.0,10.0,10.0,10.0,10.0,9.0,39,0.46 +14058,93.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.27 +42319,93.0,10.0,10.0,9.0,10.0,10.0,9.0,67,1.75 +44970,97.0,10.0,10.0,10.0,10.0,9.0,10.0,67,0.82 +38755,98.0,10.0,10.0,10.0,10.0,10.0,9.0,8,0.16 +62135,95.0,10.0,9.0,10.0,10.0,10.0,10.0,37,0.75 +65948,88.0,9.0,8.0,9.0,9.0,10.0,9.0,100,1.18 +53347,91.0,9.0,9.0,10.0,10.0,10.0,9.0,17,0.28 +65506,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.02 +49181,,,,,,,,0, +9219,,,,,,,,0, +53927,98.0,10.0,10.0,9.0,10.0,9.0,9.0,17,0.21 +31148,99.0,10.0,10.0,10.0,10.0,10.0,10.0,74,0.95 +64665,90.0,9.0,10.0,9.0,9.0,9.0,9.0,25,0.34 +62620,94.0,9.0,9.0,10.0,10.0,10.0,9.0,16,0.23 +57908,95.0,10.0,10.0,10.0,10.0,9.0,9.0,343,4.26 +22623,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.05 +24279,93.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.14 +61273,88.0,9.0,9.0,9.0,9.0,9.0,9.0,120,1.51 +69882,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +46698,98.0,10.0,10.0,10.0,10.0,9.0,10.0,150,1.89 +38657,,,,,,,,0, +62223,86.0,9.0,8.0,10.0,9.0,9.0,9.0,160,2.11 +62211,,,,,,,,0, +69763,97.0,9.0,10.0,10.0,10.0,9.0,9.0,201,2.49 +57220,,,,,,,,0, +70959,,,,,,,,0, +24928,,,,,,,,0, +14185,80.0,8.0,6.0,10.0,10.0,10.0,7.0,2,0.21 +29920,83.0,8.0,8.0,9.0,9.0,8.0,8.0,23,0.36 +11888,,,,,,,,0, +66198,,,,,,,,0, +55265,96.0,10.0,9.0,10.0,10.0,10.0,10.0,11,0.81 +25548,,,,,,,,0, +48917,100.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.06 +20524,96.0,10.0,9.0,10.0,10.0,9.0,9.0,18,0.22 +25889,97.0,10.0,10.0,10.0,10.0,10.0,10.0,56,1.99 +71387,85.0,9.0,8.0,9.0,9.0,10.0,8.0,193,2.39 +50007,91.0,9.0,9.0,9.0,9.0,10.0,9.0,96,1.26 +63296,80.0,10.0,9.0,9.0,10.0,9.0,9.0,6,0.08 +1245,85.0,9.0,8.0,9.0,9.0,9.0,9.0,34,0.5 +27694,96.0,10.0,9.0,10.0,10.0,9.0,9.0,24,0.45 +15821,94.0,9.0,10.0,10.0,10.0,9.0,9.0,84,1.04 +21340,98.0,10.0,10.0,10.0,10.0,10.0,10.0,91,1.11 +45164,97.0,9.0,10.0,10.0,10.0,10.0,9.0,18,0.24 +55707,93.0,10.0,10.0,10.0,10.0,9.0,9.0,93,1.18 +26825,,,,,,,,0, +30759,88.0,9.0,8.0,10.0,10.0,9.0,9.0,52,0.65 +26566,,,,,,,,0, +53300,92.0,9.0,9.0,10.0,10.0,9.0,9.0,10,0.15 +29055,98.0,10.0,10.0,10.0,10.0,10.0,9.0,53,0.66 +14143,,,,,,,,0, +60535,,,,,,,,0, +31446,96.0,10.0,10.0,10.0,10.0,9.0,9.0,11,0.35 +63186,98.0,10.0,10.0,10.0,10.0,10.0,9.0,9,0.66 +58248,92.0,10.0,9.0,10.0,10.0,9.0,9.0,47,0.58 +3928,93.0,9.0,9.0,10.0,10.0,9.0,9.0,133,1.63 +61718,95.0,10.0,9.0,10.0,10.0,10.0,9.0,12,0.2 +45061,96.0,10.0,10.0,10.0,10.0,10.0,10.0,440,5.39 +9738,95.0,10.0,9.0,10.0,10.0,10.0,9.0,29,0.47 +59108,,,,,,,,0, +25137,90.0,9.0,8.0,10.0,10.0,9.0,9.0,116,1.42 +19720,,,,,,,,0, +32096,,,,,,,,0, +41284,95.0,10.0,10.0,10.0,10.0,10.0,9.0,83,1.31 +59532,95.0,10.0,10.0,10.0,10.0,9.0,9.0,122,1.51 +28342,97.0,10.0,10.0,10.0,10.0,9.0,10.0,246,3.01 +61194,93.0,9.0,10.0,9.0,10.0,9.0,9.0,27,0.33 +2390,91.0,10.0,8.0,10.0,10.0,10.0,9.0,34,0.63 +50943,97.0,10.0,9.0,10.0,10.0,10.0,10.0,35,0.43 +1464,98.0,10.0,10.0,10.0,10.0,10.0,10.0,94,1.17 +47810,100.0,2.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +73246,98.0,10.0,10.0,10.0,10.0,9.0,10.0,10,0.13 +21708,,,,,,,,0, +5480,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.04 +18325,92.0,10.0,9.0,10.0,9.0,9.0,9.0,30,0.44 +25749,,,,,,,,0, +76729,96.0,10.0,8.0,10.0,10.0,10.0,9.0,10,0.15 +30461,91.0,9.0,10.0,10.0,9.0,9.0,9.0,21,0.27 +75029,97.0,10.0,10.0,10.0,10.0,9.0,10.0,156,1.92 +5819,92.0,9.0,9.0,9.0,10.0,10.0,9.0,161,1.99 +21106,95.0,10.0,9.0,10.0,10.0,9.0,9.0,58,0.74 +58955,97.0,10.0,10.0,10.0,10.0,9.0,9.0,19,0.35 +30363,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.08 +12974,85.0,8.0,8.0,8.0,8.0,8.0,8.0,42,0.52 +4819,91.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +45465,,,,,,,,0, +10811,,,,,,,,0, +24042,,,,,,,,0, +62917,90.0,9.0,10.0,10.0,10.0,10.0,10.0,2,0.45 +39980,,,,,,,,0, +63824,99.0,10.0,10.0,10.0,10.0,10.0,10.0,286,3.55 +28814,99.0,10.0,10.0,10.0,10.0,10.0,10.0,102,1.27 +75959,,,,,,,,0, +44067,96.0,10.0,10.0,10.0,10.0,10.0,9.0,236,2.97 +3980,,,,,,,,0, +37297,93.0,9.0,9.0,10.0,10.0,9.0,9.0,169,2.14 +11190,86.0,9.0,8.0,9.0,9.0,9.0,9.0,129,1.74 +46614,87.0,9.0,9.0,9.0,9.0,9.0,8.0,3,0.04 +22804,,,,,,,,0, +19451,,,,,,,,0, +63952,,,,,,,,0, +16187,93.0,9.0,9.0,10.0,10.0,9.0,9.0,133,1.9 +72768,97.0,9.0,9.0,10.0,10.0,9.0,9.0,7,0.42 +32811,,,,,,,,0, +26349,97.0,10.0,9.0,10.0,10.0,9.0,10.0,35,0.54 +9169,94.0,9.0,9.0,10.0,10.0,8.0,9.0,54,0.67 +26104,86.0,9.0,8.0,9.0,10.0,9.0,9.0,73,0.96 +21717,,,,,,,,0, +41579,99.0,10.0,10.0,10.0,10.0,10.0,10.0,72,0.92 +7202,94.0,10.0,10.0,10.0,10.0,10.0,9.0,17,0.22 +43090,,,,,,,,0, +24698,93.0,9.0,9.0,10.0,10.0,9.0,10.0,3,0.15 +70399,100.0,10.0,9.0,10.0,10.0,9.0,9.0,13,0.17 +50222,89.0,9.0,9.0,9.0,9.0,9.0,9.0,39,0.49 +34345,98.0,10.0,9.0,10.0,10.0,9.0,9.0,23,0.33 +35414,,,,,,,,0, +55690,91.0,9.0,9.0,10.0,10.0,9.0,9.0,7,0.42 +2915,90.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.06 +46073,88.0,9.0,9.0,9.0,10.0,9.0,10.0,15,0.22 +7998,99.0,10.0,10.0,10.0,10.0,10.0,10.0,41,0.52 +41774,92.0,9.0,9.0,10.0,10.0,10.0,9.0,16,0.26 +48919,,,,,,,,0, +72628,,,,,,,,0, +52234,,,,,,,,0, +8089,93.0,9.0,9.0,10.0,10.0,9.0,9.0,276,3.57 +18936,97.0,10.0,10.0,9.0,10.0,10.0,9.0,42,0.54 +5833,98.0,10.0,10.0,10.0,10.0,9.0,9.0,66,0.83 +49989,92.0,9.0,9.0,10.0,10.0,10.0,9.0,22,0.4 +69391,85.0,9.0,9.0,9.0,9.0,8.0,9.0,56,0.77 +36278,98.0,10.0,10.0,10.0,10.0,9.0,10.0,68,1.03 +18723,80.0,6.0,6.0,10.0,10.0,10.0,8.0,1,0.02 +23716,98.0,10.0,10.0,10.0,10.0,9.0,10.0,176,2.21 +33319,99.0,10.0,10.0,10.0,10.0,10.0,10.0,28,0.44 +75157,,,,,,,,0, +26662,,,,,,,,0, +36180,,,,,,,,0, +11854,91.0,8.0,10.0,10.0,10.0,8.0,10.0,1,0.02 +32170,91.0,9.0,9.0,10.0,10.0,9.0,9.0,389,4.88 +70355,72.0,7.0,6.0,8.0,9.0,8.0,6.0,7,1.4 +51642,91.0,9.0,9.0,10.0,10.0,9.0,9.0,93,1.25 +13055,97.0,9.0,10.0,10.0,10.0,10.0,9.0,7,0.11 +18734,90.0,9.0,9.0,10.0,10.0,10.0,9.0,3,0.05 +70165,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.17 +26732,97.0,10.0,10.0,10.0,10.0,9.0,10.0,100,1.28 +71059,99.0,10.0,10.0,10.0,10.0,10.0,10.0,82,1.05 +58041,97.0,10.0,10.0,10.0,10.0,9.0,9.0,49,0.63 +3327,94.0,9.0,10.0,10.0,10.0,10.0,9.0,87,1.13 +55078,97.0,10.0,10.0,10.0,10.0,10.0,10.0,233,2.94 +15973,99.0,10.0,10.0,10.0,10.0,10.0,10.0,60,0.8 +50983,,,,,,,,0, +51491,94.0,10.0,9.0,10.0,10.0,9.0,9.0,97,1.23 +32906,90.0,9.0,9.0,9.0,9.0,10.0,8.0,44,0.58 +8171,89.0,9.0,8.0,10.0,10.0,10.0,9.0,13,0.3 +16911,90.0,9.0,9.0,10.0,10.0,10.0,9.0,98,1.26 +30018,87.0,9.0,9.0,9.0,9.0,9.0,9.0,214,2.78 +15605,98.0,10.0,9.0,10.0,10.0,10.0,10.0,10,0.25 +47793,,,,,,,,0, +43283,,,,,,,,0, +76515,100.0,10.0,10.0,,10.0,,,1,0.06 +26453,95.0,10.0,9.0,10.0,10.0,9.0,10.0,118,1.56 +61949,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.01 +71734,,,,,,,,0, +53554,97.0,10.0,10.0,10.0,10.0,9.0,9.0,23,0.31 +45295,87.0,9.0,8.0,10.0,10.0,9.0,9.0,19,0.33 +55198,99.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.39 +15602,93.0,9.0,9.0,10.0,10.0,9.0,9.0,45,0.68 +66890,95.0,9.0,9.0,10.0,9.0,9.0,9.0,82,1.05 +57286,98.0,10.0,10.0,10.0,10.0,10.0,9.0,28,0.42 +6797,94.0,10.0,10.0,10.0,10.0,9.0,9.0,175,2.53 +25217,90.0,9.0,8.0,9.0,9.0,9.0,9.0,34,0.44 +58105,92.0,9.0,9.0,10.0,9.0,9.0,9.0,51,0.71 +873,97.0,10.0,10.0,10.0,10.0,10.0,10.0,56,0.73 +28423,,,,,,,,0, +12047,,,,,,,,0, +46642,99.0,10.0,10.0,10.0,10.0,10.0,10.0,43,1.36 +47404,,,,,,,,0, +21016,86.0,9.0,8.0,9.0,10.0,10.0,9.0,54,0.77 +36733,96.0,9.0,9.0,10.0,10.0,9.0,9.0,36,0.66 +54251,,6.0,6.0,2.0,4.0,8.0,6.0,2,0.03 +38981,,,,,,,,0, +56022,92.0,9.0,9.0,9.0,10.0,9.0,9.0,251,3.18 +6838,96.0,10.0,10.0,10.0,10.0,9.0,10.0,23,0.3 +68727,93.0,9.0,10.0,10.0,10.0,9.0,9.0,11,0.18 +17886,91.0,9.0,9.0,10.0,10.0,8.0,9.0,11,0.14 +24081,91.0,9.0,9.0,9.0,9.0,9.0,9.0,46,0.59 +4627,97.0,10.0,10.0,10.0,10.0,9.0,10.0,194,2.6 +43430,,,,,,,,0, +38039,96.0,10.0,10.0,10.0,10.0,10.0,9.0,177,2.36 +19635,,,,,,,,0, +44269,93.0,9.0,9.0,10.0,10.0,9.0,9.0,18,0.27 +26992,95.0,10.0,10.0,10.0,10.0,9.0,10.0,34,0.44 +22746,94.0,10.0,9.0,10.0,10.0,9.0,9.0,66,0.98 +41396,97.0,9.0,10.0,10.0,10.0,9.0,10.0,47,0.93 +64916,93.0,9.0,9.0,10.0,9.0,10.0,9.0,62,0.81 +45754,90.0,9.0,8.0,9.0,9.0,9.0,8.0,6,0.09 +24921,,,,,,,,0, +6682,95.0,10.0,10.0,10.0,10.0,10.0,9.0,196,2.67 +72843,,,,,,,,0, +66352,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.03 +9728,80.0,9.0,8.0,9.0,9.0,10.0,8.0,13,0.19 +20095,,,,,,,,0, +20792,97.0,10.0,10.0,10.0,10.0,10.0,10.0,68,0.94 +41136,89.0,9.0,9.0,9.0,9.0,9.0,9.0,16,0.54 +24052,,,,,,,,0, +74968,,,,,,,,0, +34728,,,,,,,,0, +7699,97.0,10.0,9.0,10.0,10.0,10.0,9.0,40,0.52 +36206,80.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.15 +1056,96.0,10.0,10.0,10.0,10.0,10.0,10.0,77,1.01 +21021,98.0,10.0,10.0,10.0,10.0,10.0,9.0,10,0.16 +32877,94.0,10.0,8.0,10.0,10.0,9.0,9.0,25,0.32 +24371,80.0,9.0,8.0,9.0,10.0,8.0,8.0,8,0.13 +64156,,,,,,,,0, +55378,,,,,,,,0, +70603,,10.0,8.0,10.0,6.0,6.0,10.0,1,0.01 +76442,92.0,9.0,10.0,10.0,10.0,10.0,9.0,10,0.14 +15714,98.0,10.0,10.0,10.0,10.0,9.0,10.0,74,1.02 +13904,90.0,10.0,9.0,9.0,9.0,9.0,9.0,14,0.21 +57728,,,,,,,,0, +67104,,,,,,,,0, +32160,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.06 +21354,94.0,9.0,9.0,10.0,10.0,10.0,9.0,128,2.15 +18478,90.0,9.0,9.0,9.0,9.0,9.0,9.0,64,0.83 +20622,96.0,10.0,10.0,10.0,10.0,9.0,10.0,108,1.42 +3345,,,,,,,,0, +56243,96.0,9.0,10.0,10.0,10.0,10.0,10.0,6,0.08 +54869,,,,,,,,1,0.02 +74281,,,,,,,,0, +6116,93.0,10.0,9.0,10.0,10.0,9.0,9.0,11,0.15 +67772,99.0,10.0,10.0,10.0,10.0,10.0,10.0,40,0.58 +67142,99.0,10.0,10.0,10.0,10.0,10.0,10.0,501,6.5 +66593,88.0,9.0,8.0,10.0,10.0,9.0,9.0,114,1.51 +17492,90.0,9.0,9.0,10.0,10.0,10.0,8.0,8,0.13 +8130,99.0,10.0,10.0,10.0,10.0,10.0,10.0,67,0.88 +34516,95.0,10.0,10.0,10.0,10.0,9.0,9.0,25,0.36 +36923,,,,,,,,0, +40563,99.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.18 +39287,87.0,9.0,8.0,9.0,10.0,8.0,9.0,220,3.04 +13775,96.0,10.0,10.0,10.0,10.0,9.0,9.0,51,0.69 +33088,100.0,10.0,10.0,10.0,10.0,10.0,10.0,43,0.6 +20290,,,,,,,,0, +48660,88.0,9.0,9.0,9.0,9.0,9.0,9.0,28,0.37 +12633,98.0,10.0,10.0,10.0,10.0,10.0,10.0,122,1.61 +43629,95.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.07 +6108,95.0,10.0,10.0,10.0,10.0,10.0,9.0,8,0.12 +25883,99.0,10.0,10.0,10.0,10.0,10.0,10.0,30,0.53 +2549,94.0,9.0,9.0,10.0,10.0,9.0,9.0,59,0.79 +55978,90.0,9.0,9.0,10.0,10.0,9.0,9.0,21,0.3 +75017,,,,,,,,0, +74500,93.0,9.0,10.0,9.0,10.0,10.0,9.0,71,0.97 +67866,88.0,10.0,10.0,9.0,10.0,8.0,9.0,12,0.16 +3183,88.0,9.0,9.0,9.0,9.0,9.0,9.0,335,4.49 +31400,83.0,9.0,9.0,7.0,8.0,9.0,8.0,15,0.24 +10451,98.0,10.0,10.0,10.0,10.0,10.0,10.0,208,2.77 +7783,,,,,,,,0, +71401,90.0,9.0,9.0,9.0,10.0,10.0,9.0,44,0.58 +75461,97.0,10.0,10.0,10.0,10.0,10.0,10.0,140,2.24 +14556,97.0,10.0,10.0,10.0,10.0,10.0,10.0,53,0.7 +42218,96.0,10.0,10.0,10.0,10.0,8.0,10.0,130,1.77 +17175,85.0,9.0,8.0,10.0,10.0,9.0,9.0,20,0.26 +54051,96.0,10.0,10.0,10.0,10.0,9.0,9.0,41,0.54 +74153,96.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.19 +44037,97.0,9.0,10.0,10.0,10.0,10.0,10.0,7,0.12 +75967,95.0,9.0,9.0,10.0,10.0,9.0,9.0,34,0.46 +13729,97.0,10.0,10.0,10.0,10.0,10.0,10.0,53,0.73 +15293,70.0,8.0,7.0,9.0,9.0,9.0,6.0,3,0.08 +66884,97.0,10.0,10.0,10.0,10.0,9.0,10.0,361,4.85 +27490,97.0,10.0,10.0,10.0,10.0,9.0,9.0,54,0.93 +17525,91.0,10.0,10.0,10.0,10.0,9.0,9.0,9,0.14 +2840,85.0,9.0,10.0,10.0,10.0,9.0,10.0,9,0.15 +12150,99.0,10.0,10.0,10.0,10.0,10.0,10.0,215,2.87 +73006,91.0,10.0,9.0,10.0,10.0,9.0,9.0,7,0.12 +64586,94.0,9.0,9.0,9.0,10.0,9.0,10.0,28,0.65 +22471,86.0,9.0,9.0,9.0,10.0,8.0,9.0,32,0.53 +30328,97.0,10.0,10.0,10.0,10.0,9.0,10.0,24,0.86 +17099,97.0,10.0,10.0,10.0,10.0,10.0,10.0,139,1.86 +6907,91.0,9.0,9.0,9.0,10.0,9.0,9.0,46,0.65 +47661,95.0,9.0,9.0,9.0,9.0,10.0,9.0,15,0.2 +50250,95.0,10.0,10.0,10.0,10.0,10.0,9.0,52,0.7 +50045,98.0,10.0,10.0,10.0,10.0,9.0,10.0,54,0.73 +61177,93.0,10.0,9.0,10.0,10.0,9.0,9.0,107,1.48 +47306,70.0,9.0,8.0,8.0,9.0,9.0,9.0,4,0.06 +42860,96.0,10.0,10.0,10.0,10.0,10.0,9.0,24,0.34 +69839,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.36 +55222,96.0,10.0,10.0,10.0,10.0,9.0,10.0,50,0.68 +69433,99.0,10.0,10.0,10.0,10.0,10.0,10.0,65,0.89 +18553,96.0,10.0,10.0,10.0,10.0,9.0,10.0,35,0.52 +58603,98.0,10.0,10.0,9.0,9.0,9.0,9.0,14,0.21 +71609,97.0,10.0,10.0,10.0,10.0,10.0,9.0,58,0.78 +774,,,,,,,,0, +24532,93.0,10.0,9.0,10.0,10.0,9.0,10.0,23,0.34 +34260,99.0,10.0,10.0,10.0,10.0,10.0,10.0,85,1.5 +74574,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.04 +5147,,,,,,,,0, +15116,,,,,,,,0, +44337,93.0,10.0,8.0,9.0,10.0,9.0,9.0,17,0.23 +66752,97.0,10.0,9.0,10.0,10.0,10.0,10.0,409,5.57 +36230,,,,,,,,0, +47511,92.0,9.0,9.0,10.0,10.0,9.0,9.0,19,0.44 +48638,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.17 +53360,93.0,10.0,10.0,10.0,9.0,10.0,9.0,8,0.33 +41394,94.0,9.0,10.0,10.0,10.0,10.0,9.0,18,0.77 +52150,,,,,,,,0, +23650,100.0,9.0,9.0,10.0,10.0,9.0,8.0,3,0.04 +29710,,,,,,,,0, +14578,96.0,10.0,10.0,9.0,10.0,9.0,10.0,24,0.35 +33703,90.0,9.0,9.0,10.0,10.0,10.0,9.0,33,0.49 +48668,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.33 +76664,90.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.09 +34987,94.0,9.0,9.0,10.0,10.0,8.0,9.0,96,1.33 +13543,89.0,9.0,9.0,9.0,9.0,9.0,9.0,57,1.04 +45634,97.0,10.0,10.0,10.0,10.0,10.0,10.0,43,1.68 +60089,94.0,10.0,10.0,10.0,10.0,10.0,9.0,72,1.09 +43501,95.0,9.0,9.0,10.0,10.0,9.0,9.0,19,0.45 +50985,,,,,,,,0, +11121,75.0,7.0,7.0,10.0,9.0,7.0,8.0,4,0.06 +42873,100.0,8.0,10.0,10.0,10.0,8.0,10.0,1,0.07 +72786,80.0,10.0,10.0,8.0,8.0,6.0,8.0,1,0.03 +1069,94.0,10.0,9.0,10.0,10.0,9.0,10.0,32,0.45 +19758,92.0,9.0,9.0,10.0,10.0,10.0,9.0,48,0.75 +73498,93.0,9.0,9.0,10.0,10.0,9.0,10.0,55,0.77 +67427,20.0,2.0,2.0,4.0,2.0,6.0,2.0,2,0.12 +76178,99.0,10.0,10.0,10.0,10.0,10.0,10.0,30,0.48 +10213,,,,,,,,0, +31938,96.0,10.0,10.0,10.0,10.0,10.0,10.0,148,2.1 +38979,98.0,10.0,10.0,10.0,10.0,10.0,10.0,54,0.85 +511,95.0,10.0,9.0,10.0,10.0,10.0,9.0,35,0.52 +13144,83.0,9.0,8.0,9.0,9.0,8.0,8.0,258,3.74 +50700,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.12 +31558,95.0,10.0,9.0,10.0,10.0,10.0,9.0,125,1.78 +71722,97.0,10.0,10.0,10.0,10.0,10.0,10.0,52,0.76 +30810,92.0,9.0,9.0,10.0,10.0,9.0,9.0,31,0.51 +28173,97.0,10.0,10.0,10.0,10.0,9.0,10.0,81,1.15 +12857,95.0,10.0,10.0,10.0,10.0,10.0,10.0,151,2.18 +34003,100.0,10.0,9.0,10.0,10.0,10.0,10.0,11,1.3 +28990,97.0,10.0,10.0,10.0,10.0,10.0,9.0,80,1.45 +58017,97.0,10.0,9.0,10.0,10.0,10.0,9.0,40,0.57 +63590,91.0,10.0,9.0,9.0,9.0,9.0,9.0,148,2.33 +53906,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.16 +71992,94.0,10.0,9.0,10.0,10.0,9.0,9.0,141,2.01 +68554,95.0,10.0,10.0,9.0,9.0,9.0,10.0,17,0.25 +16922,95.0,10.0,10.0,10.0,10.0,9.0,9.0,230,3.31 +6403,94.0,10.0,10.0,10.0,10.0,9.0,10.0,420,6.01 +27049,93.0,9.0,10.0,10.0,10.0,10.0,9.0,56,0.81 +37036,95.0,10.0,9.0,10.0,10.0,9.0,9.0,36,0.53 +27602,96.0,10.0,10.0,10.0,10.0,10.0,10.0,281,4.04 +19729,89.0,8.0,7.0,9.0,9.0,8.0,8.0,8,0.12 +68844,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,1.31 +74895,91.0,9.0,9.0,10.0,10.0,9.0,9.0,276,4.0 +45721,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.13 +66167,95.0,9.0,10.0,10.0,10.0,10.0,9.0,66,0.94 +6919,79.0,9.0,7.0,9.0,9.0,9.0,8.0,48,0.73 +33607,90.0,9.0,9.0,10.0,10.0,9.0,9.0,165,2.38 +47327,87.0,9.0,9.0,9.0,9.0,9.0,9.0,349,5.05 +28660,97.0,9.0,9.0,10.0,10.0,10.0,9.0,12,0.18 +31752,91.0,9.0,9.0,9.0,10.0,9.0,9.0,20,0.32 +43385,99.0,10.0,10.0,10.0,10.0,9.0,10.0,76,1.12 +55227,,,,,,,,3,0.2 +28427,92.0,9.0,9.0,10.0,10.0,9.0,9.0,324,4.7 +14894,94.0,10.0,10.0,10.0,10.0,9.0,9.0,334,4.9 +28859,94.0,9.0,10.0,10.0,10.0,10.0,9.0,307,4.5 +34975,94.0,10.0,10.0,10.0,10.0,10.0,10.0,339,4.94 +53392,87.0,9.0,9.0,9.0,10.0,9.0,8.0,3,0.12 +73597,99.0,10.0,10.0,10.0,10.0,10.0,10.0,166,2.41 +52685,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.3 +42222,98.0,10.0,10.0,10.0,10.0,10.0,10.0,63,0.93 +61925,93.0,9.0,10.0,10.0,10.0,8.0,9.0,99,1.47 +55947,96.0,10.0,10.0,10.0,10.0,9.0,9.0,90,1.32 +69379,94.0,10.0,9.0,10.0,10.0,9.0,9.0,35,0.51 +69242,94.0,10.0,9.0,9.0,10.0,9.0,9.0,24,0.4 +23346,96.0,10.0,9.0,10.0,10.0,9.0,9.0,10,0.15 +10373,99.0,10.0,10.0,10.0,10.0,10.0,10.0,91,1.69 +72119,,,,,,,,0, +71983,96.0,10.0,10.0,10.0,10.0,9.0,10.0,5,4.69 +10074,89.0,9.0,9.0,10.0,10.0,9.0,9.0,162,2.43 +57324,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +33313,91.0,9.0,9.0,10.0,10.0,9.0,9.0,55,1.27 +58976,93.0,10.0,9.0,10.0,10.0,10.0,9.0,109,2.1 +129,85.0,9.0,8.0,9.0,9.0,9.0,9.0,111,1.63 +48180,98.0,10.0,10.0,10.0,10.0,9.0,10.0,56,0.92 +25175,99.0,10.0,10.0,10.0,10.0,9.0,10.0,204,2.99 +49504,84.0,9.0,8.0,9.0,9.0,10.0,8.0,39,0.58 +30792,100.0,10.0,10.0,10.0,10.0,10.0,10.0,45,1.14 +45153,85.0,10.0,8.0,10.0,10.0,10.0,9.0,9,0.13 +51356,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.16 +27023,98.0,10.0,10.0,10.0,10.0,9.0,10.0,189,2.81 +53476,91.0,9.0,9.0,9.0,9.0,10.0,9.0,54,0.79 +73596,84.0,9.0,9.0,9.0,10.0,10.0,9.0,29,0.43 +41589,88.0,9.0,9.0,9.0,9.0,10.0,9.0,41,0.6 +34146,88.0,9.0,9.0,10.0,9.0,9.0,9.0,155,2.33 +1350,93.0,10.0,10.0,10.0,10.0,9.0,9.0,28,0.89 +61747,94.0,10.0,9.0,10.0,10.0,9.0,9.0,10,0.15 +61765,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.1 +23141,97.0,10.0,10.0,10.0,10.0,10.0,10.0,141,2.11 +67374,90.0,9.0,9.0,9.0,9.0,9.0,9.0,31,0.48 +69301,95.0,9.0,9.0,10.0,10.0,10.0,10.0,365,5.36 +25737,92.0,10.0,10.0,9.0,10.0,9.0,9.0,76,1.14 +76216,90.0,9.0,9.0,9.0,10.0,10.0,9.0,55,0.83 +17304,96.0,10.0,10.0,10.0,10.0,9.0,10.0,86,1.31 +10564,96.0,10.0,8.0,10.0,10.0,9.0,9.0,22,0.33 +60003,96.0,10.0,10.0,10.0,10.0,9.0,10.0,80,2.0 +7773,96.0,9.0,10.0,10.0,10.0,9.0,10.0,9,0.17 +18305,96.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.06 +61052,85.0,9.0,7.0,10.0,10.0,9.0,9.0,33,0.49 +61559,94.0,10.0,10.0,10.0,10.0,9.0,10.0,22,0.33 +36490,94.0,9.0,9.0,10.0,10.0,10.0,9.0,267,4.02 +43117,99.0,10.0,10.0,10.0,10.0,10.0,10.0,86,2.59 +32400,89.0,9.0,9.0,9.0,9.0,9.0,9.0,18,0.28 +59191,96.0,10.0,10.0,10.0,10.0,9.0,10.0,100,1.5 +64124,99.0,10.0,10.0,10.0,10.0,10.0,10.0,211,3.16 +76782,94.0,9.0,10.0,10.0,10.0,9.0,9.0,44,0.66 +40426,,,,,,,,0, +14797,94.0,10.0,10.0,10.0,10.0,9.0,9.0,139,2.61 +75750,98.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.27 +59343,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.06 +75305,99.0,10.0,10.0,10.0,10.0,10.0,10.0,236,3.54 +3428,98.0,10.0,10.0,10.0,10.0,9.0,10.0,47,0.72 +54733,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,1.01 +21828,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.02 +36546,89.0,9.0,8.0,10.0,10.0,10.0,9.0,137,2.09 +1406,94.0,9.0,10.0,10.0,10.0,9.0,9.0,339,5.12 +33888,100.0,10.0,9.0,9.0,9.0,10.0,10.0,3,0.05 +11906,95.0,10.0,10.0,10.0,10.0,10.0,10.0,35,0.53 +50444,95.0,10.0,9.0,10.0,10.0,10.0,9.0,146,2.28 +24401,96.0,10.0,10.0,10.0,10.0,9.0,10.0,15,0.24 +7027,92.0,9.0,9.0,10.0,10.0,9.0,9.0,167,2.53 +70543,,,,,,,,0, +34810,93.0,10.0,9.0,10.0,10.0,10.0,9.0,176,2.73 +52907,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,3.06 +15129,96.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.08 +38295,95.0,10.0,8.0,10.0,9.0,8.0,9.0,4,0.06 +72004,95.0,10.0,9.0,10.0,10.0,10.0,9.0,127,2.23 +21823,,,,,,,,0, +41278,,,,,,,,0, +4823,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.34 +16707,97.0,10.0,10.0,10.0,10.0,10.0,10.0,67,1.84 +7949,90.0,9.0,9.0,10.0,10.0,9.0,9.0,120,1.84 +15134,91.0,9.0,9.0,10.0,10.0,9.0,9.0,110,1.71 +47787,90.0,9.0,9.0,9.0,10.0,9.0,9.0,2,0.17 +50726,93.0,9.0,10.0,10.0,10.0,9.0,9.0,347,5.26 +61153,95.0,10.0,9.0,10.0,10.0,9.0,10.0,72,1.09 +13848,91.0,10.0,10.0,10.0,10.0,9.0,9.0,137,2.09 +20650,93.0,9.0,9.0,10.0,10.0,9.0,9.0,13,0.26 +37734,,,,,,,,0, +70802,98.0,10.0,9.0,10.0,10.0,10.0,10.0,36,0.63 +36830,,,,,,,,0, +64832,98.0,9.0,10.0,10.0,10.0,9.0,9.0,18,0.3 +1769,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.09 +1854,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.14 +53209,97.0,10.0,10.0,10.0,10.0,10.0,10.0,26,0.4 +47016,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.17 +35926,90.0,9.0,9.0,10.0,10.0,9.0,9.0,84,1.31 +7073,99.0,10.0,10.0,10.0,10.0,10.0,10.0,60,0.94 +3070,98.0,10.0,10.0,10.0,10.0,10.0,10.0,49,0.76 +3130,80.0,10.0,8.0,8.0,10.0,10.0,10.0,1,1.0 +34188,93.0,10.0,9.0,10.0,10.0,8.0,9.0,22,0.53 +57779,95.0,10.0,10.0,10.0,10.0,9.0,10.0,16,0.87 +52307,,,,,,,,0, +65038,84.0,9.0,8.0,10.0,10.0,8.0,9.0,10,0.15 +38794,95.0,10.0,9.0,10.0,10.0,10.0,9.0,61,0.95 +16063,94.0,10.0,10.0,10.0,10.0,10.0,9.0,221,3.53 +28139,90.0,10.0,9.0,9.0,9.0,9.0,9.0,23,0.36 +73821,97.0,10.0,10.0,10.0,10.0,9.0,10.0,21,0.33 +60210,100.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.11 +704,85.0,10.0,10.0,10.0,10.0,9.0,9.0,6,0.1 +40215,94.0,9.0,9.0,10.0,10.0,10.0,9.0,47,0.76 +23382,97.0,10.0,10.0,10.0,10.0,10.0,10.0,185,2.91 +76542,86.0,9.0,10.0,10.0,10.0,10.0,9.0,17,1.41 +16527,93.0,10.0,9.0,10.0,10.0,10.0,9.0,45,0.69 +986,,,,,,,,0, +49074,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.15 +75181,95.0,10.0,9.0,10.0,10.0,9.0,9.0,17,0.31 +66941,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.16 +20222,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +17787,95.0,10.0,10.0,10.0,10.0,8.0,10.0,4,0.06 +73517,,,,,,,,0, +34849,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.25 +4210,96.0,10.0,9.0,10.0,10.0,10.0,10.0,88,1.39 +41856,98.0,10.0,10.0,10.0,10.0,10.0,9.0,47,0.75 +72755,,,,,,,,0, +73868,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.04 +225,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.27 +38468,92.0,9.0,10.0,10.0,10.0,10.0,9.0,58,0.91 +76406,94.0,9.0,9.0,10.0,10.0,10.0,9.0,67,1.05 +10859,96.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.83 +73613,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.11 +47558,,,,,,,,0, +26574,91.0,9.0,9.0,10.0,10.0,10.0,9.0,31,0.5 +66937,91.0,10.0,9.0,10.0,10.0,8.0,9.0,156,2.44 +12365,94.0,9.0,9.0,10.0,10.0,9.0,9.0,177,3.07 +42162,93.0,10.0,10.0,10.0,10.0,9.0,9.0,22,0.56 +14681,97.0,9.0,9.0,10.0,10.0,9.0,9.0,33,0.52 +68514,90.0,9.0,9.0,9.0,10.0,9.0,9.0,11,0.19 +51424,,,,,,,,0, +10644,91.0,9.0,9.0,10.0,9.0,8.0,9.0,45,0.78 +62602,93.0,9.0,9.0,10.0,9.0,10.0,10.0,3,2.43 +1282,97.0,10.0,10.0,10.0,10.0,9.0,9.0,121,1.91 +51965,89.0,9.0,9.0,10.0,10.0,9.0,9.0,14,0.25 +23608,99.0,10.0,10.0,10.0,10.0,10.0,10.0,21,1.1 +29465,96.0,10.0,10.0,10.0,10.0,9.0,10.0,26,2.11 +55137,99.0,10.0,10.0,10.0,10.0,10.0,10.0,76,1.19 +68472,100.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.1 +59371,80.0,8.0,10.0,10.0,10.0,10.0,6.0,1,0.23 +17549,94.0,9.0,9.0,10.0,10.0,10.0,10.0,8,0.13 +58621,92.0,9.0,9.0,10.0,10.0,10.0,9.0,48,0.75 +31595,96.0,10.0,9.0,10.0,10.0,10.0,10.0,14,0.25 +49445,97.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.39 +27263,99.0,10.0,10.0,10.0,10.0,10.0,10.0,51,0.81 +70309,60.0,8.0,6.0,8.0,8.0,8.0,6.0,2,0.35 +23252,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.06 +66472,87.0,9.0,8.0,10.0,10.0,9.0,9.0,184,3.06 +34908,85.0,8.0,9.0,8.0,8.0,9.0,9.0,27,0.48 +45771,81.0,8.0,9.0,9.0,9.0,9.0,8.0,18,0.31 +27336,92.0,10.0,9.0,10.0,10.0,9.0,9.0,35,0.55 +52053,92.0,10.0,9.0,10.0,10.0,9.0,9.0,35,0.56 +8342,81.0,8.0,8.0,9.0,9.0,9.0,8.0,38,0.6 +72014,88.0,10.0,9.0,9.0,9.0,9.0,9.0,21,0.42 +33957,98.0,10.0,10.0,10.0,10.0,10.0,10.0,149,2.38 +19834,96.0,10.0,10.0,10.0,10.0,10.0,10.0,190,3.88 +69363,93.0,9.0,9.0,10.0,10.0,9.0,9.0,40,0.64 +57616,93.0,9.0,9.0,10.0,10.0,10.0,9.0,31,0.5 +72175,97.0,10.0,10.0,10.0,10.0,9.0,10.0,15,0.25 +53472,96.0,10.0,10.0,10.0,9.0,9.0,10.0,6,0.26 +3262,98.0,10.0,10.0,10.0,10.0,10.0,9.0,69,1.12 +5263,95.0,10.0,10.0,10.0,10.0,10.0,10.0,127,2.02 +10377,98.0,10.0,10.0,10.0,10.0,10.0,10.0,60,1.06 +54404,94.0,10.0,9.0,10.0,10.0,10.0,9.0,91,1.45 +36045,94.0,10.0,9.0,10.0,10.0,9.0,9.0,77,1.86 +5577,98.0,10.0,10.0,10.0,9.0,9.0,9.0,11,0.17 +13666,94.0,10.0,9.0,10.0,10.0,9.0,9.0,170,2.7 +8309,95.0,9.0,9.0,10.0,10.0,9.0,9.0,41,0.68 +25690,,,,,,,,0, +46438,94.0,9.0,9.0,10.0,10.0,9.0,9.0,29,0.47 +39812,95.0,9.0,10.0,10.0,10.0,10.0,10.0,68,1.18 +5614,89.0,9.0,9.0,9.0,10.0,10.0,9.0,53,0.86 +11153,89.0,9.0,9.0,9.0,10.0,9.0,9.0,79,1.26 +69937,99.0,10.0,10.0,10.0,10.0,9.0,10.0,93,1.5 +27136,98.0,10.0,10.0,10.0,10.0,10.0,10.0,132,2.12 +7798,97.0,10.0,10.0,10.0,10.0,10.0,9.0,45,0.74 +11775,94.0,10.0,9.0,9.0,10.0,10.0,10.0,7,0.95 +53816,87.0,9.0,8.0,10.0,10.0,10.0,9.0,11,0.19 +23354,88.0,9.0,8.0,10.0,10.0,10.0,9.0,15,0.25 +52588,96.0,10.0,10.0,9.0,10.0,9.0,10.0,25,1.7 +7183,94.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.24 +5529,93.0,9.0,9.0,10.0,10.0,10.0,9.0,99,1.6 +17829,96.0,9.0,9.0,9.0,9.0,9.0,9.0,15,0.28 +42944,94.0,10.0,10.0,10.0,10.0,10.0,10.0,50,1.56 +13696,94.0,10.0,9.0,10.0,10.0,10.0,9.0,39,1.36 +29191,99.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.29 +75785,92.0,10.0,10.0,9.0,10.0,9.0,10.0,45,0.72 +126,90.0,10.0,9.0,9.0,10.0,10.0,9.0,12,0.19 +44131,95.0,10.0,10.0,10.0,10.0,10.0,9.0,92,1.49 +50578,97.0,10.0,10.0,10.0,10.0,10.0,9.0,32,0.56 +1019,98.0,10.0,10.0,10.0,10.0,10.0,10.0,47,0.78 +46819,95.0,10.0,10.0,10.0,10.0,9.0,10.0,76,1.25 +56085,93.0,9.0,9.0,10.0,10.0,10.0,10.0,12,0.2 +71067,94.0,9.0,9.0,10.0,10.0,10.0,9.0,7,0.17 +22563,96.0,10.0,9.0,10.0,10.0,9.0,10.0,43,1.02 +7741,92.0,10.0,10.0,10.0,10.0,8.0,9.0,5,0.1 +70902,98.0,10.0,10.0,10.0,10.0,8.0,9.0,10,0.17 +37769,95.0,10.0,10.0,10.0,10.0,9.0,9.0,41,0.96 +9091,93.0,10.0,9.0,10.0,10.0,10.0,9.0,22,0.41 +22065,100.0,10.0,9.0,10.0,10.0,10.0,9.0,4,0.18 +8653,90.0,9.0,9.0,9.0,9.0,10.0,9.0,82,1.52 +8077,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.08 +62480,,,,,,,,0, +4649,,,,,,,,0, +57746,,,,,,,,0, +59404,89.0,9.0,9.0,9.0,9.0,10.0,9.0,65,1.06 +23143,100.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.15 +34061,100.0,10.0,10.0,10.0,10.0,9.0,9.0,7,0.13 +59305,91.0,9.0,9.0,9.0,9.0,9.0,9.0,11,0.19 +42529,96.0,10.0,10.0,10.0,10.0,10.0,9.0,101,1.68 +57399,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +7842,99.0,10.0,10.0,10.0,10.0,9.0,9.0,46,1.59 +57167,97.0,10.0,10.0,10.0,10.0,9.0,10.0,24,0.58 +48141,98.0,10.0,10.0,10.0,10.0,10.0,10.0,33,0.89 +11376,91.0,9.0,9.0,10.0,10.0,9.0,9.0,71,1.18 +54171,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.18 +56202,78.0,9.0,7.0,10.0,10.0,10.0,8.0,12,0.2 +73229,98.0,10.0,10.0,10.0,10.0,9.0,9.0,80,1.33 +8663,96.0,10.0,10.0,10.0,10.0,9.0,10.0,46,0.79 +1525,91.0,10.0,9.0,9.0,10.0,8.0,9.0,17,0.28 +32980,,,,,,,,0, +2748,95.0,10.0,9.0,10.0,10.0,10.0,10.0,63,1.06 +39045,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.04 +57771,,,,,,,,0, +77096,99.0,10.0,10.0,10.0,10.0,10.0,10.0,68,1.68 +27169,92.0,9.0,9.0,9.0,10.0,9.0,9.0,94,1.54 +19940,88.0,9.0,8.0,10.0,10.0,10.0,10.0,5,0.1 +51401,96.0,10.0,10.0,10.0,10.0,9.0,10.0,19,0.37 +31502,90.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.04 +47795,97.0,10.0,10.0,10.0,10.0,10.0,10.0,101,1.66 +71147,96.0,10.0,10.0,10.0,10.0,10.0,9.0,141,2.35 +32821,97.0,10.0,9.0,10.0,10.0,10.0,10.0,77,1.4 +65561,74.0,7.0,8.0,9.0,8.0,9.0,8.0,17,0.28 +70128,60.0,8.0,8.0,8.0,7.0,7.0,7.0,5,0.09 +25145,80.0,8.0,9.0,8.0,8.0,9.0,8.0,23,0.38 +39917,82.0,8.0,8.0,9.0,10.0,9.0,8.0,22,0.39 +2788,98.0,10.0,10.0,10.0,10.0,9.0,10.0,8,0.15 +5601,94.0,10.0,9.0,9.0,10.0,10.0,9.0,57,0.95 +27796,,,,,,,,0, +8860,91.0,10.0,9.0,10.0,10.0,8.0,10.0,19,0.38 +6852,89.0,9.0,8.0,10.0,9.0,9.0,9.0,16,0.28 +9708,90.0,9.0,9.0,10.0,9.0,10.0,9.0,6,0.1 +34534,80.0,10.0,10.0,10.0,9.0,8.0,9.0,3,0.05 +15449,100.0,10.0,10.0,10.0,9.0,9.0,9.0,9,0.16 +13665,96.0,10.0,9.0,10.0,10.0,10.0,9.0,306,5.03 +33109,98.0,10.0,10.0,10.0,10.0,10.0,10.0,76,1.27 +41228,93.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.07 +14569,94.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.34 +49276,94.0,10.0,10.0,10.0,10.0,10.0,10.0,99,1.76 +20559,88.0,9.0,9.0,9.0,10.0,8.0,9.0,27,0.44 +69609,91.0,9.0,9.0,10.0,10.0,9.0,9.0,11,0.19 +51893,,,,,,,,0, +68600,99.0,10.0,10.0,10.0,10.0,10.0,10.0,22,0.58 +32678,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +20570,96.0,10.0,10.0,9.0,10.0,10.0,9.0,14,0.23 +32145,81.0,8.0,8.0,9.0,9.0,9.0,8.0,56,0.92 +64879,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +6244,91.0,9.0,10.0,10.0,10.0,10.0,10.0,22,0.87 +27429,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.27 +48916,85.0,9.0,9.0,9.0,9.0,9.0,9.0,4,0.07 +11239,97.0,10.0,10.0,10.0,10.0,9.0,9.0,14,0.25 +46949,93.0,9.0,9.0,10.0,10.0,10.0,9.0,193,3.27 +16716,95.0,10.0,10.0,10.0,10.0,9.0,10.0,93,1.57 +16202,91.0,9.0,9.0,10.0,10.0,10.0,9.0,17,0.62 +28176,,,,,,,,1,0.2 +10680,80.0,10.0,8.0,4.0,8.0,10.0,8.0,1,0.02 +59119,76.0,8.0,8.0,8.0,8.0,9.0,8.0,67,1.11 +53582,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.15 +51765,86.0,9.0,9.0,10.0,9.0,10.0,9.0,60,1.08 +67365,95.0,10.0,9.0,10.0,10.0,9.0,9.0,32,0.53 +61333,93.0,10.0,9.0,9.0,10.0,9.0,9.0,45,0.78 +53436,97.0,10.0,9.0,10.0,10.0,10.0,9.0,41,0.69 +69737,92.0,10.0,8.0,9.0,10.0,8.0,10.0,5,0.08 +71794,89.0,9.0,8.0,9.0,10.0,10.0,9.0,23,0.41 +49289,,,,,,,,0, +62949,,,,,,,,0, +9782,86.0,9.0,9.0,9.0,9.0,9.0,9.0,113,2.01 +18099,,,,,,,,0, +41459,95.0,10.0,10.0,10.0,10.0,8.0,9.0,96,1.62 +5424,,,,,,,,0, +6731,97.0,10.0,10.0,9.0,10.0,10.0,10.0,7,0.12 +22637,100.0,10.0,9.0,10.0,10.0,10.0,10.0,5,0.18 +60786,96.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.19 +70993,90.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.08 +7419,98.0,10.0,10.0,10.0,10.0,9.0,10.0,99,1.72 +50401,,,,,,,,0, +10689,93.0,9.0,10.0,10.0,10.0,9.0,9.0,237,4.02 +5561,96.0,10.0,10.0,10.0,10.0,9.0,9.0,49,0.85 +50213,98.0,10.0,10.0,10.0,10.0,9.0,10.0,88,1.55 +46232,95.0,10.0,9.0,10.0,10.0,10.0,9.0,19,0.33 +42690,85.0,9.0,8.0,9.0,9.0,9.0,8.0,70,1.18 +3530,97.0,10.0,10.0,10.0,10.0,9.0,10.0,16,0.27 +40579,93.0,9.0,10.0,10.0,10.0,9.0,9.0,4,0.08 +15876,94.0,10.0,10.0,10.0,10.0,9.0,9.0,85,1.43 +39375,97.0,10.0,9.0,10.0,10.0,9.0,10.0,60,1.03 +4238,99.0,10.0,10.0,10.0,10.0,10.0,10.0,26,0.47 +31862,100.0,9.0,6.0,10.0,10.0,10.0,8.0,3,0.07 +34484,94.0,10.0,10.0,10.0,10.0,10.0,9.0,132,2.24 +70971,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +7177,92.0,9.0,8.0,10.0,10.0,9.0,9.0,5,0.09 +23877,93.0,9.0,9.0,10.0,10.0,10.0,9.0,107,2.59 +22672,93.0,9.0,9.0,10.0,10.0,9.0,9.0,89,1.56 +52561,94.0,10.0,9.0,10.0,10.0,10.0,10.0,69,1.18 +12932,95.0,10.0,9.0,10.0,10.0,10.0,9.0,12,0.21 +13030,97.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.34 +30281,95.0,10.0,9.0,10.0,10.0,10.0,9.0,57,0.96 +26984,97.0,10.0,10.0,10.0,10.0,9.0,9.0,116,2.04 +14885,95.0,10.0,9.0,10.0,10.0,10.0,9.0,27,0.48 +4728,97.0,10.0,10.0,10.0,10.0,10.0,10.0,23,0.4 +61219,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.2 +10745,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.56 +38079,80.0,8.0,8.0,8.0,10.0,10.0,6.0,3,0.07 +42156,99.0,10.0,10.0,10.0,10.0,9.0,10.0,81,1.49 +55466,91.0,9.0,10.0,10.0,10.0,9.0,9.0,18,0.45 +33739,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +8232,81.0,9.0,9.0,9.0,9.0,9.0,9.0,22,0.5 +3522,87.0,8.0,9.0,10.0,9.0,10.0,9.0,18,0.31 +28198,96.0,10.0,9.0,10.0,10.0,10.0,10.0,14,0.6 +42688,100.0,9.0,10.0,10.0,10.0,9.0,10.0,3,0.06 +19325,100.0,8.0,8.0,9.0,9.0,10.0,9.0,2,0.03 +54805,95.0,10.0,10.0,10.0,10.0,9.0,9.0,71,1.29 +74118,,,,,,,,0, +22697,100.0,10.0,10.0,10.0,10.0,9.0,9.0,12,0.21 +76011,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.47 +40345,93.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.59 +13710,81.0,8.0,8.0,9.0,8.0,9.0,8.0,163,2.82 +70087,84.0,9.0,8.0,9.0,9.0,8.0,8.0,97,1.69 +62112,92.0,10.0,9.0,10.0,10.0,9.0,9.0,107,1.91 +57751,97.0,10.0,10.0,10.0,10.0,10.0,10.0,101,1.76 +39021,92.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.16 +31627,,,,,,,,0, +64053,80.0,6.0,4.0,10.0,10.0,8.0,8.0,1,0.02 +29435,90.0,10.0,9.0,10.0,10.0,9.0,9.0,53,2.8 +31816,99.0,10.0,9.0,10.0,10.0,9.0,9.0,16,0.28 +44273,100.0,10.0,10.0,10.0,10.0,10.0,9.0,16,0.28 +66046,98.0,10.0,10.0,10.0,10.0,10.0,10.0,97,1.68 +1458,96.0,9.0,9.0,10.0,10.0,10.0,9.0,23,0.41 +72070,72.0,8.0,7.0,8.0,8.0,10.0,7.0,40,0.7 +57480,,,,,,,,0, +75275,87.0,9.0,9.0,9.0,9.0,9.0,9.0,74,1.27 +29979,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +15387,94.0,10.0,10.0,10.0,10.0,10.0,9.0,40,0.73 +29316,94.0,10.0,9.0,10.0,10.0,10.0,10.0,27,0.48 +35312,91.0,10.0,9.0,10.0,10.0,10.0,9.0,45,0.8 +5744,83.0,9.0,8.0,9.0,9.0,9.0,9.0,146,2.54 +13369,98.0,10.0,9.0,10.0,10.0,10.0,9.0,32,0.55 +42724,,,,,,,,0, +68674,100.0,9.0,9.0,10.0,10.0,9.0,10.0,2,0.87 +41206,97.0,9.0,10.0,10.0,10.0,9.0,9.0,8,0.14 +50104,91.0,10.0,9.0,10.0,10.0,10.0,10.0,72,1.24 +21639,92.0,10.0,10.0,10.0,10.0,9.0,9.0,17,0.45 +69171,,,,,,,,0, +35867,95.0,10.0,10.0,10.0,10.0,9.0,10.0,76,1.35 +41115,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +5401,85.0,9.0,8.0,9.0,9.0,9.0,9.0,113,2.0 +32107,93.0,10.0,9.0,10.0,10.0,9.0,10.0,9,0.16 +10527,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.03 +43677,,,,,,,,0, +65256,93.0,10.0,10.0,10.0,10.0,10.0,10.0,73,1.37 +59926,,,,,,,,0, +25243,94.0,9.0,10.0,9.0,9.0,9.0,10.0,11,0.21 +30499,97.0,10.0,10.0,10.0,10.0,9.0,10.0,82,1.43 +30214,93.0,9.0,9.0,10.0,10.0,9.0,9.0,141,2.49 +24623,96.0,10.0,10.0,10.0,10.0,9.0,9.0,38,0.68 +9456,93.0,9.0,10.0,10.0,10.0,9.0,9.0,3,0.06 +18603,96.0,9.0,10.0,10.0,10.0,10.0,10.0,20,0.36 +48167,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.18 +70679,,,,,,,,0, +75051,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.14 +26913,91.0,10.0,10.0,10.0,10.0,9.0,9.0,17,0.43 +72490,92.0,10.0,9.0,10.0,10.0,9.0,9.0,85,1.52 +7084,95.0,9.0,9.0,10.0,10.0,10.0,10.0,8,0.65 +7355,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.21 +57577,97.0,10.0,10.0,10.0,10.0,10.0,10.0,80,1.41 +479,96.0,10.0,9.0,10.0,10.0,9.0,10.0,96,2.0 +55233,85.0,10.0,9.0,9.0,10.0,9.0,9.0,8,0.15 +26038,92.0,10.0,9.0,9.0,10.0,9.0,9.0,19,0.63 +32563,94.0,10.0,10.0,10.0,10.0,9.0,9.0,117,2.15 +63617,84.0,9.0,8.0,9.0,9.0,9.0,9.0,12,0.21 +64984,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.02 +56827,94.0,10.0,10.0,10.0,10.0,9.0,9.0,421,7.39 +35949,76.0,8.0,7.0,9.0,8.0,9.0,8.0,30,0.56 +5855,94.0,10.0,10.0,10.0,10.0,9.0,9.0,335,5.88 +26420,95.0,10.0,10.0,10.0,10.0,9.0,9.0,393,6.93 +62277,93.0,9.0,9.0,10.0,10.0,9.0,9.0,383,6.74 +54536,93.0,9.0,9.0,10.0,10.0,9.0,9.0,42,0.91 +4626,85.0,10.0,9.0,10.0,9.0,9.0,9.0,4,0.08 +67352,,,,,,,,0, +45471,96.0,10.0,10.0,9.0,10.0,9.0,8.0,5,0.44 +69910,98.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.27 +23676,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.07 +69987,94.0,10.0,9.0,10.0,10.0,10.0,9.0,184,3.29 +53084,99.0,10.0,10.0,10.0,10.0,10.0,10.0,65,1.15 +21552,96.0,10.0,10.0,10.0,10.0,9.0,9.0,30,1.06 +66067,95.0,10.0,9.0,10.0,10.0,10.0,9.0,137,2.41 +4800,98.0,10.0,10.0,10.0,10.0,10.0,10.0,180,4.23 +74476,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +49037,60.0,10.0,6.0,10.0,10.0,8.0,8.0,1,0.03 +67486,97.0,10.0,10.0,10.0,10.0,9.0,9.0,6,0.11 +31183,89.0,9.0,9.0,10.0,10.0,10.0,9.0,34,0.6 +33349,97.0,10.0,10.0,10.0,10.0,9.0,10.0,117,2.06 +69416,,,,,,,,0, +50875,87.0,9.0,7.0,10.0,10.0,9.0,9.0,3,0.06 +56600,95.0,10.0,9.0,10.0,10.0,9.0,10.0,62,1.11 +47184,99.0,10.0,10.0,10.0,10.0,9.0,10.0,31,0.56 +14944,91.0,9.0,10.0,10.0,10.0,9.0,9.0,7,0.13 +36391,96.0,9.0,10.0,10.0,10.0,9.0,10.0,11,1.09 +56813,98.0,10.0,10.0,10.0,10.0,9.0,10.0,218,4.03 +19069,97.0,10.0,10.0,10.0,10.0,10.0,9.0,89,5.15 +32931,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.08 +58379,95.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.46 +32491,88.0,10.0,9.0,9.0,10.0,9.0,9.0,16,0.3 +59223,89.0,9.0,8.0,9.0,9.0,9.0,9.0,133,2.36 +39692,97.0,10.0,9.0,10.0,10.0,9.0,10.0,28,0.53 +18644,99.0,10.0,10.0,10.0,10.0,10.0,10.0,40,0.75 +25953,98.0,10.0,10.0,10.0,10.0,10.0,10.0,38,0.71 +13711,94.0,10.0,10.0,10.0,10.0,9.0,9.0,43,0.77 +10580,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.1 +9282,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.17 +74186,89.0,9.0,8.0,10.0,9.0,9.0,9.0,10,0.19 +68661,98.0,10.0,10.0,10.0,10.0,10.0,10.0,41,0.75 +7542,,,,,,,,0, +27636,94.0,9.0,10.0,10.0,10.0,10.0,9.0,176,3.2 +828,94.0,10.0,10.0,9.0,10.0,9.0,10.0,89,2.31 +71030,94.0,10.0,10.0,10.0,10.0,9.0,9.0,145,2.59 +22054,100.0,10.0,10.0,10.0,10.0,9.0,10.0,47,1.6 +27657,95.0,10.0,9.0,10.0,10.0,9.0,9.0,4,0.08 +35727,98.0,10.0,10.0,10.0,10.0,10.0,10.0,112,2.04 +73407,96.0,10.0,10.0,10.0,10.0,10.0,10.0,140,2.59 +66913,80.0,9.0,8.0,9.0,9.0,10.0,9.0,53,0.99 +22582,99.0,10.0,10.0,10.0,10.0,10.0,10.0,23,0.45 +74686,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.12 +60679,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,0.07 +52797,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.08 +75223,98.0,10.0,10.0,10.0,10.0,10.0,10.0,185,3.51 +41989,95.0,9.0,9.0,10.0,10.0,10.0,9.0,20,0.4 +32992,94.0,9.0,9.0,10.0,10.0,9.0,9.0,25,0.49 +6530,96.0,9.0,10.0,10.0,10.0,10.0,10.0,5,0.1 +46541,96.0,10.0,10.0,10.0,10.0,9.0,10.0,84,1.51 +22778,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.12 +64662,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +31739,,,,,,,,5,0.16 +25709,62.0,7.0,6.0,7.0,7.0,9.0,6.0,10,0.19 +7851,99.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.51 +1250,88.0,9.0,8.0,10.0,9.0,8.0,9.0,90,1.63 +25973,99.0,10.0,10.0,9.0,10.0,9.0,10.0,28,0.51 +33954,98.0,10.0,10.0,10.0,10.0,10.0,10.0,39,0.71 +42801,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +58355,95.0,10.0,10.0,10.0,10.0,9.0,10.0,66,1.41 +68089,89.0,10.0,9.0,9.0,10.0,9.0,9.0,9,0.27 +27865,99.0,10.0,10.0,10.0,10.0,10.0,10.0,147,2.66 +47571,99.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.28 +31588,97.0,10.0,10.0,10.0,10.0,9.0,10.0,27,0.5 +20156,96.0,10.0,10.0,10.0,10.0,9.0,10.0,213,4.21 +5299,,,,,,,,0, +31340,81.0,8.0,8.0,9.0,9.0,9.0,8.0,43,0.91 +35481,91.0,9.0,9.0,10.0,10.0,9.0,9.0,28,0.73 +33814,94.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.24 +59909,97.0,10.0,10.0,10.0,10.0,9.0,10.0,39,0.77 +48495,90.0,9.0,9.0,9.0,8.0,8.0,9.0,7,0.3 +68134,96.0,10.0,9.0,10.0,10.0,10.0,9.0,52,0.94 +23613,93.0,10.0,8.0,9.0,9.0,8.0,9.0,20,0.38 +16132,,,,,,,,0, +68949,96.0,10.0,9.0,10.0,10.0,10.0,10.0,17,2.74 +37910,98.0,10.0,10.0,10.0,10.0,9.0,9.0,10,0.19 +14633,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.22 +69610,,,,,,,,0, +7108,96.0,10.0,9.0,10.0,10.0,9.0,9.0,34,1.04 +44825,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.05 +48240,84.0,9.0,8.0,8.0,8.0,9.0,9.0,5,0.09 +67965,,,,,,,,0, +73151,90.0,9.0,9.0,10.0,10.0,9.0,9.0,205,3.73 +14780,100.0,10.0,10.0,10.0,10.0,9.0,10.0,28,0.53 +22351,97.0,10.0,9.0,10.0,10.0,10.0,9.0,6,0.11 +71860,,,,,,,,0, +76807,91.0,9.0,9.0,10.0,10.0,9.0,9.0,23,0.42 +53161,90.0,9.0,7.0,10.0,9.0,10.0,9.0,2,0.04 +29263,95.0,10.0,9.0,10.0,10.0,10.0,10.0,33,0.6 +57835,,,,,,,,0, +38165,99.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.62 +8890,100.0,10.0,10.0,9.0,10.0,10.0,10.0,3,0.06 +6551,97.0,10.0,10.0,10.0,10.0,9.0,10.0,56,1.02 +29738,88.0,9.0,9.0,9.0,9.0,9.0,9.0,118,3.46 +53600,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.13 +53490,91.0,9.0,8.0,9.0,10.0,10.0,9.0,7,0.22 +13831,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.22 +64943,80.0,8.0,9.0,9.0,9.0,9.0,8.0,48,1.02 +27995,94.0,10.0,9.0,10.0,10.0,9.0,10.0,295,5.47 +68171,93.0,10.0,9.0,10.0,10.0,9.0,9.0,45,1.04 +74198,86.0,8.0,9.0,9.0,10.0,9.0,9.0,42,0.78 +16570,96.0,10.0,9.0,10.0,10.0,10.0,10.0,26,0.49 +12900,95.0,9.0,9.0,10.0,10.0,9.0,9.0,11,0.21 +52123,84.0,9.0,8.0,9.0,9.0,8.0,9.0,91,1.67 +48434,99.0,10.0,10.0,10.0,10.0,10.0,10.0,58,1.32 +20740,98.0,9.0,10.0,10.0,10.0,10.0,9.0,15,0.29 +53256,99.0,10.0,10.0,10.0,10.0,10.0,9.0,19,0.35 +25562,100.0,9.0,9.0,10.0,10.0,9.0,9.0,2,0.04 +67399,95.0,10.0,10.0,9.0,10.0,10.0,9.0,37,0.68 +55517,80.0,8.0,7.0,9.0,9.0,9.0,9.0,81,1.49 +36911,92.0,10.0,9.0,10.0,10.0,10.0,9.0,28,0.55 +44267,88.0,9.0,9.0,10.0,10.0,9.0,9.0,123,2.27 +50052,86.0,9.0,9.0,9.0,10.0,9.0,9.0,185,3.39 +61914,88.0,9.0,9.0,9.0,9.0,9.0,9.0,183,3.35 +46802,86.0,9.0,9.0,9.0,9.0,9.0,9.0,138,2.61 +18135,91.0,9.0,9.0,10.0,10.0,9.0,9.0,109,2.0 +56049,92.0,9.0,9.0,10.0,10.0,10.0,9.0,31,0.62 +59719,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.15 +58168,97.0,10.0,9.0,10.0,10.0,10.0,10.0,25,1.29 +21970,92.0,9.0,9.0,10.0,9.0,9.0,9.0,127,2.4 +6103,88.0,9.0,8.0,9.0,9.0,10.0,8.0,11,0.22 +18438,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.1 +55673,92.0,9.0,9.0,10.0,10.0,10.0,10.0,13,0.24 +19323,94.0,10.0,10.0,10.0,10.0,10.0,9.0,122,2.29 +44195,,,,,,,,0, +63015,73.0,7.0,7.0,8.0,7.0,7.0,6.0,6,0.12 +54557,86.0,9.0,9.0,10.0,9.0,9.0,9.0,15,0.8 +26741,,,,,,,,0, +28616,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +32247,91.0,9.0,9.0,10.0,10.0,9.0,9.0,295,5.5 +52239,91.0,10.0,9.0,9.0,9.0,9.0,9.0,11,0.29 +42076,100.0,9.0,9.0,10.0,10.0,9.0,9.0,3,0.07 +23239,100.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.24 +56321,88.0,9.0,9.0,10.0,10.0,9.0,9.0,88,1.62 +19294,96.0,10.0,9.0,10.0,10.0,10.0,10.0,59,1.08 +8822,,,,,,,,0, +71312,99.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.92 +49426,,,,,,,,0, +45445,98.0,10.0,10.0,10.0,10.0,10.0,10.0,31,0.59 +8085,92.0,9.0,9.0,10.0,9.0,9.0,9.0,43,0.8 +14551,91.0,9.0,9.0,10.0,10.0,10.0,9.0,113,2.11 +8751,94.0,10.0,10.0,10.0,10.0,9.0,9.0,79,1.52 +43383,96.0,10.0,9.0,10.0,10.0,9.0,9.0,194,3.61 +11503,93.0,9.0,10.0,10.0,10.0,8.0,9.0,114,2.18 +69084,79.0,8.0,8.0,8.0,8.0,8.0,8.0,27,0.64 +65023,,,,,,,,0, +21805,94.0,10.0,9.0,10.0,10.0,9.0,9.0,43,0.83 +18104,90.0,9.0,8.0,9.0,9.0,10.0,10.0,4,0.08 +48685,78.0,9.0,9.0,9.0,8.0,9.0,8.0,12,0.22 +72053,83.0,9.0,8.0,9.0,9.0,8.0,8.0,20,0.38 +27760,99.0,10.0,10.0,10.0,10.0,10.0,10.0,23,0.55 +1463,95.0,10.0,10.0,10.0,10.0,10.0,9.0,79,1.46 +1927,92.0,9.0,9.0,10.0,10.0,9.0,9.0,56,1.04 +49323,90.0,9.0,9.0,10.0,10.0,10.0,9.0,16,0.3 +21768,,,,,,,,0, +45437,91.0,10.0,10.0,10.0,10.0,9.0,9.0,140,3.84 +73892,,,,,,,,0, +18470,97.0,10.0,10.0,10.0,10.0,9.0,9.0,19,0.37 +41205,100.0,10.0,9.0,9.0,10.0,9.0,9.0,2,0.04 +44979,94.0,10.0,10.0,10.0,10.0,8.0,9.0,144,2.67 +29270,99.0,10.0,10.0,10.0,10.0,9.0,10.0,18,0.35 +58118,96.0,10.0,9.0,10.0,10.0,9.0,9.0,19,0.36 +18633,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.13 +22586,98.0,10.0,9.0,10.0,10.0,10.0,10.0,38,0.71 +17874,97.0,10.0,10.0,10.0,10.0,10.0,10.0,43,0.86 +62256,90.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.04 +49774,77.0,9.0,9.0,9.0,8.0,9.0,7.0,7,0.18 +34103,98.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.28 +76516,92.0,10.0,9.0,9.0,9.0,10.0,9.0,76,1.41 +32077,98.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.32 +47232,96.0,10.0,10.0,10.0,10.0,10.0,9.0,15,0.33 +10143,,,,,,,,0, +74251,64.0,8.0,8.0,9.0,8.0,9.0,7.0,11,0.24 +73348,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +35425,98.0,10.0,9.0,10.0,10.0,10.0,9.0,8,0.46 +74518,95.0,10.0,10.0,10.0,10.0,10.0,9.0,52,0.98 +594,97.0,10.0,10.0,10.0,10.0,9.0,10.0,33,1.48 +19674,92.0,10.0,9.0,10.0,10.0,9.0,9.0,20,0.37 +40332,91.0,10.0,9.0,9.0,10.0,8.0,9.0,7,0.17 +34172,,,,,,,,1,0.67 +33338,,,,,,,,0, +51464,85.0,9.0,8.0,9.0,9.0,10.0,8.0,16,0.3 +38507,,,,,,,,0, +50744,91.0,9.0,9.0,9.0,9.0,9.0,9.0,148,2.77 +51281,93.0,10.0,9.0,10.0,10.0,9.0,9.0,111,2.07 +57359,85.0,10.0,8.0,9.0,9.0,9.0,9.0,12,0.23 +56630,99.0,10.0,10.0,10.0,10.0,10.0,10.0,28,0.53 +17654,,,,,,,,0, +18662,97.0,10.0,10.0,10.0,10.0,9.0,9.0,57,1.53 +73841,83.0,9.0,8.0,9.0,10.0,9.0,8.0,14,0.27 +7483,99.0,10.0,10.0,10.0,10.0,10.0,10.0,29,0.57 +66163,,,,,,,,0, +58159,100.0,10.0,10.0,10.0,10.0,10.0,9.0,15,0.28 +26907,83.0,8.0,8.0,8.0,10.0,8.0,8.0,1,0.02 +55643,,,,,,,,1,0.03 +61525,85.0,9.0,9.0,10.0,10.0,9.0,9.0,11,0.21 +30494,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +14091,93.0,10.0,10.0,10.0,10.0,10.0,9.0,128,2.39 +41738,90.0,8.0,9.0,9.0,10.0,8.0,10.0,2,0.05 +12860,100.0,9.0,10.0,10.0,9.0,10.0,10.0,4,0.08 +74259,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.07 +58763,94.0,10.0,9.0,10.0,10.0,9.0,9.0,77,1.46 +5438,91.0,9.0,9.0,10.0,10.0,9.0,9.0,37,0.7 +53529,90.0,9.0,8.0,10.0,10.0,10.0,9.0,39,0.73 +57209,94.0,10.0,10.0,10.0,10.0,9.0,9.0,42,0.83 +9522,73.0,6.0,9.0,9.0,10.0,7.0,7.0,4,0.11 +73329,97.0,10.0,9.0,10.0,10.0,10.0,10.0,42,0.78 +31086,97.0,10.0,9.0,10.0,10.0,9.0,9.0,54,1.01 +35317,98.0,10.0,10.0,10.0,10.0,10.0,10.0,90,2.22 +29916,100.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.05 +23416,80.0,9.0,8.0,9.0,9.0,8.0,8.0,45,0.85 +2691,90.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.1 +32993,,,,,,,,0, +37595,99.0,10.0,10.0,10.0,10.0,10.0,10.0,32,0.63 +42599,86.0,9.0,8.0,9.0,10.0,8.0,9.0,8,0.28 +37015,92.0,9.0,9.0,10.0,9.0,9.0,9.0,44,0.84 +5117,82.0,9.0,9.0,9.0,9.0,10.0,9.0,45,1.57 +13068,100.0,10.0,10.0,10.0,9.0,9.0,10.0,6,0.23 +5557,100.0,9.0,10.0,10.0,10.0,10.0,10.0,4,0.08 +75175,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +10168,98.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.68 +47295,93.0,9.0,9.0,10.0,10.0,9.0,9.0,32,0.6 +10724,92.0,9.0,10.0,10.0,10.0,10.0,9.0,15,0.29 +25426,94.0,10.0,9.0,10.0,10.0,10.0,9.0,13,0.45 +19644,96.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.8 +42657,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.28 +17187,93.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.11 +36979,,,,,,,,0, +36819,,,,,,,,0, +9988,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.16 +26968,96.0,10.0,9.0,10.0,10.0,10.0,10.0,5,0.15 +72023,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.81 +50990,,,,,,,,0, +11666,93.0,9.0,9.0,9.0,10.0,10.0,9.0,3,0.69 +66789,97.0,10.0,10.0,10.0,10.0,9.0,10.0,12,0.24 +72159,96.0,10.0,10.0,10.0,10.0,10.0,10.0,21,0.4 +8087,100.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.06 +56196,91.0,9.0,9.0,10.0,10.0,9.0,9.0,22,0.53 +67712,89.0,9.0,9.0,9.0,10.0,10.0,9.0,20,0.42 +52904,98.0,10.0,9.0,10.0,10.0,9.0,10.0,10,0.23 +5075,95.0,10.0,9.0,10.0,10.0,9.0,9.0,16,0.91 +1407,90.0,9.0,9.0,10.0,9.0,10.0,9.0,31,0.64 +4201,95.0,10.0,10.0,10.0,9.0,10.0,9.0,57,1.08 +48045,91.0,10.0,8.0,10.0,9.0,10.0,9.0,53,1.03 +55033,91.0,9.0,8.0,9.0,9.0,8.0,9.0,7,0.13 +12579,93.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.25 +30785,98.0,10.0,9.0,10.0,10.0,9.0,10.0,8,0.15 +1038,90.0,9.0,8.0,10.0,10.0,10.0,9.0,6,0.13 +20339,94.0,10.0,9.0,10.0,10.0,9.0,9.0,153,2.95 +27587,85.0,8.0,9.0,10.0,10.0,9.0,9.0,4,0.1 +47875,97.0,10.0,10.0,10.0,10.0,10.0,10.0,53,1.03 +25207,96.0,9.0,10.0,10.0,10.0,10.0,10.0,16,0.31 +41670,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.04 +14082,93.0,10.0,9.0,10.0,10.0,10.0,10.0,9,0.17 +20034,83.0,8.0,9.0,9.0,8.0,8.0,9.0,83,1.7 +5289,,,,,,,,0, +11389,,,,,,,,0, +58962,94.0,10.0,9.0,10.0,10.0,10.0,10.0,156,2.95 +75526,97.0,10.0,10.0,10.0,10.0,9.0,9.0,46,0.88 +26588,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.04 +1657,97.0,10.0,10.0,10.0,10.0,10.0,10.0,34,1.19 +34643,,,,,,,,0, +62961,98.0,10.0,10.0,10.0,10.0,9.0,10.0,81,1.57 +21580,90.0,9.0,9.0,9.0,10.0,10.0,9.0,45,0.85 +216,93.0,10.0,10.0,9.0,10.0,9.0,9.0,3,0.08 +15249,,,,,,,,0, +12014,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.3 +32938,94.0,10.0,9.0,10.0,10.0,10.0,9.0,234,4.63 +68105,74.0,8.0,7.0,9.0,9.0,9.0,8.0,56,1.06 +40534,85.0,9.0,8.0,9.0,9.0,9.0,9.0,117,2.21 +70674,95.0,10.0,9.0,9.0,10.0,10.0,10.0,105,2.07 +47538,97.0,10.0,10.0,10.0,10.0,9.0,9.0,18,0.34 +75316,,,,,,,,0, +51975,97.0,10.0,10.0,10.0,10.0,9.0,9.0,83,1.57 +54795,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.25 +10770,93.0,9.0,9.0,10.0,10.0,10.0,9.0,8,0.15 +2068,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.06 +56718,92.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.13 +15055,88.0,9.0,8.0,10.0,10.0,10.0,9.0,31,0.92 +65331,100.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.04 +16822,88.0,9.0,9.0,10.0,10.0,9.0,9.0,24,0.46 +36074,96.0,10.0,9.0,10.0,10.0,10.0,10.0,10,1.35 +14338,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.18 +69516,98.0,10.0,10.0,10.0,10.0,10.0,10.0,83,1.62 +58833,100.0,10.0,10.0,10.0,10.0,10.0,8.0,4,0.29 +61105,,,,,,,,0, +57565,98.0,10.0,9.0,10.0,10.0,10.0,9.0,11,0.31 +40887,,,,,,,,0, +17195,97.0,10.0,10.0,10.0,10.0,10.0,10.0,26,0.5 +9845,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.15 +46289,93.0,10.0,9.0,9.0,9.0,9.0,10.0,9,0.17 +28204,94.0,10.0,9.0,10.0,10.0,9.0,9.0,53,1.03 +56298,,,,,,,,0, +5630,95.0,10.0,9.0,10.0,10.0,9.0,9.0,16,0.33 +56086,91.0,9.0,10.0,10.0,10.0,10.0,9.0,165,3.15 +22013,100.0,10.0,10.0,10.0,10.0,8.0,8.0,2,0.36 +18151,99.0,10.0,10.0,10.0,10.0,10.0,10.0,115,2.82 +70437,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +6067,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +25848,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.04 +6246,,,,,,,,0, +21551,,,,,,,,0, +54766,90.0,9.0,10.0,10.0,9.0,9.0,8.0,2,0.08 +42615,,,,,,,,0, +32636,,,,,,,,0, +54079,98.0,10.0,10.0,10.0,10.0,10.0,10.0,28,0.55 +3458,82.0,9.0,8.0,9.0,9.0,9.0,9.0,97,1.91 +71856,99.0,10.0,10.0,10.0,10.0,10.0,10.0,82,1.58 +47944,96.0,10.0,9.0,9.0,10.0,10.0,10.0,16,0.31 +73839,99.0,10.0,10.0,10.0,10.0,10.0,10.0,21,1.89 +37400,98.0,10.0,10.0,10.0,10.0,10.0,10.0,165,3.29 +8685,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +22222,90.0,9.0,10.0,10.0,10.0,8.0,9.0,22,0.44 +25572,85.0,9.0,9.0,9.0,9.0,8.0,9.0,11,0.22 +6277,,,,,,,,0, +32760,100.0,10.0,9.0,9.0,10.0,10.0,10.0,2,0.04 +64875,88.0,9.0,9.0,10.0,10.0,9.0,9.0,12,0.24 +42359,,,,,,,,0, +61770,94.0,9.0,9.0,10.0,10.0,10.0,9.0,7,0.14 +44545,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.14 +39695,90.0,9.0,9.0,10.0,10.0,9.0,9.0,61,1.18 +34144,100.0,10.0,10.0,10.0,9.0,9.0,10.0,4,0.28 +74491,,,,,,,,0, +18763,85.0,8.0,9.0,10.0,9.0,9.0,8.0,8,0.45 +7846,92.0,9.0,9.0,10.0,10.0,9.0,9.0,37,0.73 +4787,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.14 +54258,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.1 +62353,89.0,9.0,10.0,10.0,10.0,10.0,9.0,24,0.5 +23377,100.0,9.0,10.0,9.0,9.0,9.0,9.0,3,0.06 +51630,,,,,,,,0, +1205,98.0,10.0,9.0,10.0,10.0,9.0,9.0,13,0.31 +74029,99.0,10.0,10.0,10.0,10.0,10.0,10.0,160,6.04 +24051,95.0,10.0,10.0,10.0,10.0,9.0,10.0,78,1.54 +18592,,,,,,,,0, +45683,99.0,10.0,10.0,10.0,10.0,10.0,10.0,31,0.83 +31447,94.0,9.0,10.0,10.0,10.0,10.0,10.0,30,1.49 +11578,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +54933,96.0,10.0,9.0,10.0,10.0,9.0,9.0,14,0.48 +8033,97.0,10.0,9.0,9.0,10.0,10.0,9.0,7,0.75 +56635,94.0,10.0,9.0,10.0,10.0,10.0,10.0,87,2.77 +59533,80.0,8.0,8.0,8.0,4.0,8.0,8.0,1,0.02 +21063,90.0,9.0,9.0,10.0,9.0,8.0,9.0,102,2.02 +31925,,,,,,,,0, +49986,97.0,10.0,9.0,10.0,10.0,10.0,9.0,29,0.56 +29818,90.0,10.0,8.0,10.0,10.0,10.0,9.0,6,0.12 +75336,100.0,10.0,10.0,10.0,10.0,9.0,9.0,6,0.12 +2884,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.18 +10965,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.85 +10275,100.0,10.0,10.0,10.0,10.0,10.0,10.0,24,0.58 +7501,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.23 +47767,91.0,9.0,9.0,10.0,10.0,10.0,9.0,14,0.33 +28277,92.0,9.0,9.0,9.0,9.0,9.0,9.0,19,0.39 +54450,,,,,,,,0, +70861,96.0,10.0,10.0,10.0,10.0,10.0,9.0,11,0.22 +9413,99.0,10.0,10.0,10.0,10.0,10.0,10.0,174,3.39 +5327,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +15459,90.0,8.0,8.0,8.0,8.0,10.0,8.0,2,0.04 +49667,,,,,,,,0, +29993,89.0,9.0,9.0,9.0,9.0,10.0,9.0,37,0.73 +5506,,,,,,,,0, +805,100.0,10.0,10.0,10.0,10.0,8.0,8.0,2,0.04 +40864,94.0,10.0,9.0,10.0,10.0,9.0,9.0,92,1.83 +5836,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +49021,97.0,10.0,9.0,9.0,10.0,9.0,9.0,16,1.1 +41782,93.0,10.0,10.0,10.0,10.0,9.0,9.0,108,2.1 +51882,,,,,,,,0, +40351,95.0,10.0,9.0,10.0,10.0,10.0,9.0,30,0.63 +10957,96.0,10.0,10.0,10.0,10.0,10.0,10.0,30,0.59 +75263,95.0,10.0,9.0,10.0,10.0,9.0,10.0,33,0.64 +3963,80.0,9.0,9.0,9.0,10.0,9.0,7.0,3,0.06 +24159,88.0,9.0,9.0,9.0,9.0,10.0,9.0,88,1.81 +59125,98.0,10.0,10.0,10.0,10.0,10.0,10.0,38,0.75 +41607,,,,,,,,0, +45700,,,,,,,,0, +62229,,,,,,,,0, +35792,98.0,10.0,10.0,10.0,10.0,10.0,10.0,120,2.35 +12774,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.09 +27357,97.0,9.0,10.0,10.0,10.0,10.0,9.0,26,0.51 +10572,96.0,10.0,10.0,10.0,10.0,9.0,10.0,32,0.64 +58227,,,,,,,,0, +45091,,,,,,,,0, +39704,95.0,10.0,10.0,10.0,10.0,10.0,9.0,129,2.63 +50601,86.0,9.0,8.0,10.0,9.0,9.0,9.0,126,2.52 +29884,,,,,,,,0, +65212,97.0,10.0,10.0,10.0,10.0,10.0,10.0,26,0.52 +8821,97.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.58 +35101,95.0,10.0,10.0,10.0,10.0,10.0,9.0,84,1.66 +15022,89.0,9.0,8.0,10.0,9.0,9.0,9.0,131,2.63 +3248,83.0,9.0,8.0,9.0,9.0,8.0,9.0,7,0.17 +7077,98.0,10.0,10.0,10.0,10.0,10.0,10.0,69,1.36 +45496,97.0,10.0,10.0,10.0,10.0,9.0,9.0,70,1.39 +51959,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.19 +42751,90.0,9.0,9.0,9.0,10.0,10.0,9.0,127,2.64 +17394,94.0,10.0,9.0,9.0,10.0,9.0,9.0,96,1.88 +29337,98.0,10.0,10.0,10.0,10.0,9.0,10.0,27,0.53 +72184,100.0,10.0,10.0,6.0,8.0,10.0,8.0,2,0.04 +38233,92.0,10.0,10.0,10.0,9.0,10.0,10.0,12,0.34 +56328,96.0,10.0,9.0,10.0,10.0,10.0,9.0,14,0.28 +25406,93.0,9.0,10.0,10.0,10.0,8.0,9.0,116,2.3 +67131,96.0,9.0,10.0,10.0,10.0,9.0,9.0,78,1.55 +66836,89.0,9.0,10.0,10.0,9.0,9.0,9.0,14,0.32 +57512,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.17 +51958,98.0,9.0,9.0,9.0,10.0,9.0,9.0,9,0.19 +11702,95.0,10.0,10.0,10.0,10.0,10.0,9.0,56,1.2 +4415,85.0,9.0,8.0,9.0,9.0,9.0,9.0,22,0.74 +12441,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.07 +21244,,,,,,,,0, +21296,100.0,10.0,10.0,10.0,10.0,,10.0,1,0.03 +70224,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.43 +40015,,,,,,,,0, +44454,97.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.3 +39160,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.3 +72457,80.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +4554,76.0,8.0,8.0,8.0,8.0,8.0,8.0,14,0.31 +1844,98.0,10.0,10.0,10.0,10.0,10.0,10.0,48,0.96 +593,87.0,9.0,9.0,9.0,9.0,9.0,9.0,100,2.02 +71809,95.0,10.0,10.0,10.0,10.0,10.0,9.0,65,1.35 +63262,95.0,10.0,10.0,10.0,10.0,10.0,10.0,138,4.12 +32900,98.0,10.0,10.0,10.0,10.0,10.0,10.0,38,1.49 +39521,97.0,10.0,10.0,10.0,10.0,10.0,10.0,51,1.03 +72242,98.0,10.0,10.0,10.0,10.0,10.0,10.0,294,5.83 +13970,83.0,9.0,8.0,10.0,10.0,8.0,9.0,8,0.52 +76129,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.32 +57572,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.37 +44622,93.0,9.0,9.0,9.0,9.0,9.0,9.0,12,0.44 +8576,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.04 +72692,93.0,9.0,9.0,10.0,10.0,10.0,9.0,33,1.2 +27813,,,,,,,,0, +53393,90.0,10.0,9.0,10.0,10.0,9.0,9.0,8,0.16 +13501,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.12 +34096,84.0,9.0,8.0,10.0,9.0,9.0,9.0,33,0.66 +1005,99.0,10.0,10.0,10.0,10.0,10.0,10.0,99,1.99 +21909,91.0,9.0,9.0,10.0,9.0,10.0,9.0,9,0.21 +8118,,,,,,,,0, +11561,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +22073,80.0,8.0,8.0,9.0,9.0,8.0,8.0,45,0.9 +28296,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +56790,,,,,,,,0, +32538,92.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.53 +70824,,,,,,,,0, +54698,91.0,9.0,8.0,10.0,9.0,10.0,9.0,38,0.76 +47744,94.0,9.0,9.0,10.0,10.0,9.0,9.0,54,1.08 +69264,91.0,9.0,8.0,10.0,10.0,10.0,9.0,20,0.4 +37642,70.0,6.0,7.0,10.0,6.0,9.0,5.0,2,0.05 +25615,99.0,10.0,10.0,10.0,10.0,10.0,10.0,33,0.67 +50290,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.13 +72458,,,,,,,,0, +62080,,,,,,,,0, +50496,,,,,,,,0, +62348,96.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.28 +71940,,,,,,,,1,0.05 +22731,92.0,10.0,9.0,9.0,10.0,9.0,9.0,50,1.0 +39352,85.0,9.0,9.0,9.0,9.0,9.0,8.0,62,1.59 +19667,91.0,10.0,9.0,10.0,10.0,9.0,9.0,16,0.62 +3701,97.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.29 +25331,96.0,10.0,9.0,10.0,10.0,10.0,9.0,24,0.55 +7751,97.0,10.0,10.0,10.0,10.0,10.0,10.0,51,1.05 +3488,95.0,10.0,9.0,10.0,10.0,9.0,9.0,31,0.63 +1840,95.0,10.0,9.0,9.0,9.0,10.0,9.0,12,0.32 +31949,80.0,6.0,8.0,10.0,10.0,10.0,8.0,1,0.02 +23856,90.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.11 +47487,93.0,10.0,9.0,10.0,10.0,9.0,9.0,53,1.09 +17746,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.37 +20768,97.0,10.0,10.0,10.0,10.0,9.0,10.0,15,0.52 +5248,94.0,10.0,10.0,10.0,10.0,9.0,10.0,27,0.85 +46722,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.02 +20392,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.42 +65524,93.0,9.0,8.0,10.0,10.0,10.0,9.0,13,0.28 +68671,98.0,10.0,10.0,10.0,10.0,10.0,10.0,29,0.67 +12836,,,,,,,,0, +69868,100.0,10.0,10.0,10.0,10.0,9.0,10.0,10,0.33 +12351,99.0,10.0,10.0,10.0,10.0,10.0,10.0,43,1.05 +44181,87.0,9.0,9.0,10.0,9.0,9.0,9.0,6,0.13 +63280,93.0,10.0,9.0,10.0,10.0,9.0,9.0,11,0.23 +59587,100.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.41 +3930,92.0,9.0,9.0,10.0,10.0,9.0,9.0,26,0.61 +20999,82.0,8.0,8.0,9.0,9.0,10.0,8.0,97,1.96 +30849,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.09 +9301,100.0,9.0,10.0,10.0,10.0,10.0,8.0,2,0.13 +25016,88.0,9.0,9.0,10.0,10.0,9.0,9.0,19,0.38 +41375,100.0,10.0,10.0,10.0,10.0,10.0,9.0,12,2.83 +38418,95.0,10.0,9.0,10.0,10.0,9.0,9.0,104,2.13 +68305,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +29500,96.0,10.0,9.0,10.0,9.0,10.0,9.0,24,0.49 +10900,73.0,8.0,7.0,6.0,7.0,9.0,8.0,3,0.09 +47561,98.0,10.0,9.0,10.0,10.0,9.0,10.0,21,0.5 +34598,98.0,10.0,10.0,10.0,10.0,10.0,10.0,22,0.53 +18853,98.0,10.0,10.0,10.0,10.0,10.0,10.0,89,1.8 +55243,97.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.14 +11352,92.0,9.0,9.0,9.0,10.0,10.0,9.0,160,3.25 +6747,,,,,,,,0, +15649,92.0,10.0,8.0,10.0,10.0,8.0,8.0,5,0.13 +55757,94.0,10.0,10.0,10.0,10.0,9.0,9.0,134,3.12 +50440,,,,,,,,0, +11127,,,,,,,,0, +73372,91.0,9.0,9.0,10.0,10.0,10.0,9.0,124,2.54 +68841,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +66604,97.0,10.0,9.0,10.0,10.0,9.0,10.0,12,0.25 +17317,94.0,9.0,9.0,10.0,10.0,10.0,9.0,24,0.5 +69718,,,,,,,,0, +19916,86.0,9.0,9.0,9.0,10.0,9.0,9.0,18,0.39 +72,,,,,,,,0, +74870,92.0,9.0,9.0,10.0,10.0,9.0,9.0,6,0.13 +14178,,,,,,,,0, +47288,90.0,9.0,8.0,10.0,9.0,10.0,9.0,77,1.57 +59944,86.0,9.0,9.0,9.0,9.0,10.0,9.0,87,1.84 +57475,99.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.02 +8586,99.0,10.0,10.0,10.0,10.0,10.0,10.0,33,0.69 +55859,95.0,10.0,10.0,10.0,10.0,9.0,10.0,95,1.94 +42613,,,,,,,,0, +67693,99.0,10.0,10.0,10.0,10.0,9.0,10.0,18,0.41 +65552,99.0,10.0,10.0,10.0,10.0,10.0,10.0,31,0.63 +2648,85.0,9.0,8.0,9.0,9.0,9.0,9.0,61,1.25 +28822,,,,,,,,0, +67342,96.0,10.0,10.0,10.0,10.0,10.0,9.0,54,1.13 +21926,80.0,10.0,6.0,7.0,10.0,9.0,9.0,2,0.04 +11536,100.0,9.0,8.0,9.0,10.0,9.0,9.0,3,0.08 +17040,,,,,,,,0, +8770,100.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.04 +30435,,,,,,,,0, +38280,89.0,9.0,8.0,9.0,9.0,9.0,9.0,23,0.47 +15975,,,,,,,,0, +36209,95.0,10.0,10.0,10.0,10.0,10.0,10.0,129,2.65 +66077,99.0,10.0,10.0,10.0,10.0,10.0,10.0,57,1.24 +36529,90.0,9.0,9.0,9.0,9.0,10.0,9.0,38,0.85 +47939,94.0,10.0,9.0,10.0,10.0,10.0,9.0,68,1.55 +73425,83.0,9.0,8.0,9.0,9.0,9.0,8.0,127,2.61 +42620,87.0,9.0,9.0,10.0,8.0,9.0,8.0,14,0.31 +24875,,,,,,,,0, +60723,89.0,10.0,9.0,10.0,10.0,10.0,9.0,13,0.27 +7471,93.0,9.0,9.0,10.0,9.0,9.0,8.0,4,0.08 +73851,96.0,10.0,10.0,10.0,10.0,10.0,9.0,28,0.59 +18231,90.0,9.0,10.0,9.0,8.0,10.0,9.0,4,0.08 +45747,86.0,9.0,8.0,10.0,10.0,9.0,9.0,82,1.71 +73983,95.0,10.0,10.0,10.0,10.0,9.0,9.0,16,0.33 +42172,93.0,10.0,9.0,9.0,9.0,9.0,9.0,70,1.43 +32655,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +68151,93.0,10.0,9.0,9.0,10.0,9.0,9.0,13,0.27 +32375,100.0,10.0,10.0,10.0,10.0,6.0,10.0,1,0.02 +507,90.0,9.0,9.0,9.0,9.0,9.0,8.0,3,0.07 +75255,,,,,,,,0, +49601,80.0,8.0,7.0,9.0,9.0,7.0,8.0,4,0.08 +5020,94.0,9.0,10.0,10.0,10.0,10.0,10.0,59,1.22 +20206,,,,,,,,0, +57048,98.0,10.0,10.0,10.0,10.0,10.0,9.0,10,0.21 +9386,98.0,10.0,10.0,10.0,10.0,10.0,10.0,41,0.86 +67509,,,,,,,,0, +8915,95.0,10.0,9.0,10.0,10.0,9.0,9.0,11,0.23 +37974,88.0,10.0,9.0,10.0,10.0,8.0,9.0,6,0.13 +49246,80.0,7.0,6.0,9.0,9.0,8.0,8.0,2,0.15 +25791,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +66858,92.0,9.0,9.0,10.0,10.0,10.0,9.0,41,0.89 +1441,97.0,10.0,10.0,10.0,10.0,9.0,10.0,60,1.4 +75376,98.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.52 +67604,,,,,,,,0, +61146,97.0,10.0,10.0,10.0,10.0,9.0,10.0,118,2.42 +7410,60.0,8.0,6.0,8.0,8.0,8.0,10.0,1,0.02 +55354,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.02 +31497,85.0,9.0,7.0,10.0,10.0,10.0,9.0,23,0.57 +64775,96.0,9.0,9.0,10.0,10.0,9.0,9.0,6,0.13 +73248,,,,,,,,0, +43286,60.0,6.0,2.0,10.0,10.0,6.0,6.0,1,0.02 +19597,93.0,10.0,9.0,10.0,10.0,9.0,9.0,35,1.12 +46721,92.0,9.0,9.0,9.0,10.0,10.0,9.0,76,1.59 +19563,100.0,10.0,10.0,10.0,10.0,9.0,10.0,12,0.25 +22958,98.0,10.0,10.0,10.0,10.0,10.0,10.0,35,0.73 +37936,95.0,9.0,10.0,10.0,10.0,10.0,9.0,68,1.53 +7600,95.0,9.0,9.0,10.0,10.0,9.0,9.0,37,0.77 +27737,95.0,10.0,9.0,10.0,10.0,10.0,9.0,79,1.65 +30034,,,,,,,,1,0.03 +48755,99.0,10.0,10.0,10.0,10.0,10.0,10.0,35,0.75 +9391,93.0,9.0,9.0,9.0,10.0,10.0,9.0,12,0.28 +66719,,,,,,,,0, +53065,,,,,,,,0, +33591,83.0,9.0,8.0,10.0,10.0,9.0,9.0,19,0.46 +73736,,,,,,,,0, +4499,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.02 +73225,81.0,9.0,8.0,9.0,9.0,10.0,8.0,19,0.59 +40542,97.0,10.0,10.0,10.0,10.0,9.0,9.0,6,0.16 +36818,97.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.14 +55881,,,,,,,,0, +47017,96.0,10.0,9.0,10.0,10.0,9.0,9.0,5,0.1 +6383,91.0,9.0,8.0,10.0,10.0,9.0,9.0,56,1.17 +67788,93.0,9.0,10.0,10.0,10.0,10.0,9.0,9,0.19 +62622,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.09 +13572,,,,,,,,0, +370,80.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.02 +48598,,,,,,,,0, +72013,,,,,,,,0, +6350,,,,,,,,0, +64515,97.0,10.0,9.0,10.0,10.0,9.0,9.0,33,0.73 +46370,96.0,10.0,9.0,10.0,10.0,9.0,9.0,21,0.68 +75777,,,,,,,,0, +25349,90.0,10.0,9.0,10.0,9.0,10.0,9.0,11,0.47 +70058,95.0,10.0,9.0,10.0,10.0,10.0,9.0,18,0.38 +55503,,,,,,,,1,0.02 +24745,97.0,10.0,10.0,10.0,10.0,10.0,10.0,31,0.71 +16040,92.0,9.0,9.0,10.0,10.0,9.0,9.0,77,1.63 +66919,,,,,,,,0, +31943,93.0,9.0,9.0,10.0,10.0,9.0,9.0,3,0.06 +20972,,,,,,,,0, +20338,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.17 +42642,90.0,9.0,9.0,10.0,9.0,9.0,9.0,154,4.06 +35840,88.0,9.0,8.0,9.0,9.0,9.0,9.0,230,4.81 +60336,91.0,9.0,8.0,9.0,10.0,9.0,9.0,33,0.73 +14904,100.0,9.0,10.0,10.0,10.0,9.0,10.0,3,0.07 +52493,,,,,,,,0, +25939,90.0,8.0,6.0,10.0,10.0,10.0,10.0,2,0.04 +9861,,,,,,,,0, +50147,60.0,10.0,2.0,8.0,10.0,10.0,6.0,1,0.02 +38930,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.09 +58070,98.0,10.0,10.0,10.0,10.0,9.0,10.0,36,0.77 +42894,,,,,,,,0, +59510,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +52764,97.0,10.0,9.0,10.0,10.0,10.0,10.0,22,0.46 +37293,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +66118,90.0,9.0,9.0,10.0,10.0,10.0,9.0,118,2.46 +4056,100.0,10.0,9.0,10.0,10.0,10.0,9.0,8,0.2 +68616,89.0,9.0,9.0,10.0,10.0,9.0,9.0,15,0.33 +76095,98.0,10.0,10.0,10.0,10.0,10.0,10.0,66,3.33 +55801,98.0,10.0,10.0,10.0,10.0,10.0,10.0,52,1.1 +12931,73.0,8.0,7.0,8.0,9.0,9.0,8.0,50,1.18 +15005,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +54663,100.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.13 +3590,97.0,10.0,9.0,10.0,10.0,10.0,10.0,37,0.84 +61587,93.0,9.0,9.0,9.0,10.0,10.0,9.0,24,0.5 +46635,,,,,,,,0, +14602,96.0,10.0,10.0,10.0,10.0,10.0,9.0,94,2.04 +28670,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.38 +41409,94.0,10.0,10.0,10.0,10.0,9.0,10.0,62,1.31 +13456,94.0,9.0,10.0,10.0,10.0,8.0,10.0,11,0.24 +30054,99.0,10.0,10.0,10.0,10.0,10.0,10.0,69,1.49 +2610,88.0,9.0,9.0,9.0,9.0,10.0,9.0,104,2.2 +52565,,,,,,,,0, +5241,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.61 +37495,99.0,10.0,9.0,10.0,10.0,10.0,9.0,21,0.47 +26820,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.2 +21475,,,,,,,,0, +15591,88.0,9.0,9.0,10.0,10.0,10.0,8.0,32,1.07 +44713,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.09 +73928,,,,,,,,0, +76060,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +35354,94.0,10.0,9.0,9.0,10.0,10.0,9.0,21,0.46 +50526,96.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.22 +19471,90.0,10.0,9.0,10.0,9.0,9.0,9.0,14,0.32 +72010,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.21 +50247,95.0,10.0,9.0,10.0,10.0,9.0,9.0,28,0.64 +28344,97.0,10.0,10.0,10.0,10.0,10.0,10.0,150,3.19 +49198,94.0,10.0,10.0,10.0,10.0,9.0,10.0,236,5.07 +75843,92.0,9.0,9.0,9.0,10.0,9.0,9.0,6,0.13 +18113,98.0,10.0,10.0,10.0,10.0,9.0,10.0,27,0.57 +50337,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +50759,,,,,,,,0, +8230,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.15 +39073,100.0,10.0,10.0,10.0,10.0,10.0,10.0,72,1.54 +13387,97.0,10.0,7.0,10.0,10.0,10.0,10.0,9,0.19 +61759,95.0,10.0,10.0,10.0,10.0,10.0,10.0,22,0.46 +5228,93.0,9.0,9.0,9.0,10.0,9.0,9.0,21,0.45 +53475,88.0,9.0,8.0,10.0,10.0,9.0,9.0,165,3.5 +65317,99.0,10.0,10.0,10.0,10.0,10.0,10.0,34,0.76 +34151,99.0,10.0,10.0,10.0,10.0,10.0,10.0,137,2.94 +11574,95.0,10.0,9.0,10.0,10.0,10.0,10.0,13,0.29 +75010,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +68060,,,,,,,,0, +74675,93.0,10.0,10.0,10.0,9.0,10.0,10.0,3,0.69 +46934,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.04 +31104,,,,,,,,0, +55891,94.0,10.0,9.0,10.0,10.0,8.0,9.0,106,2.3 +57202,100.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.35 +14202,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +6954,94.0,10.0,9.0,9.0,10.0,9.0,9.0,45,0.97 +30874,,,,,,,,0, +54506,99.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.41 +11123,,,,,,,,0, +17249,,,,,,,,0, +48963,98.0,10.0,10.0,10.0,10.0,10.0,10.0,38,0.98 +52617,99.0,10.0,10.0,10.0,10.0,10.0,10.0,156,3.42 +67982,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.11 +25892,94.0,10.0,9.0,10.0,10.0,10.0,9.0,79,1.7 +14899,91.0,9.0,10.0,10.0,10.0,10.0,9.0,22,1.5 +41215,,,,,,,,0, +49531,98.0,10.0,10.0,10.0,10.0,10.0,10.0,22,0.49 +4480,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.54 +6657,98.0,10.0,10.0,10.0,10.0,9.0,9.0,9,0.2 +42170,99.0,10.0,10.0,10.0,10.0,10.0,10.0,35,0.76 +47567,87.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.07 +42899,98.0,10.0,10.0,10.0,10.0,10.0,10.0,84,1.93 +58816,100.0,10.0,10.0,10.0,9.0,8.0,10.0,2,0.04 +13971,100.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.29 +15241,94.0,10.0,9.0,10.0,10.0,10.0,9.0,32,0.69 +35039,97.0,10.0,10.0,10.0,10.0,9.0,10.0,22,1.14 +23870,91.0,10.0,9.0,10.0,10.0,10.0,9.0,162,3.5 +75378,90.0,9.0,8.0,10.0,10.0,9.0,9.0,107,2.39 +35860,,,,,,,,0, +26358,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.35 +43521,96.0,10.0,10.0,10.0,10.0,10.0,9.0,86,2.38 +65046,96.0,10.0,9.0,10.0,10.0,10.0,9.0,37,0.89 +32490,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.13 +68834,93.0,9.0,9.0,10.0,10.0,9.0,9.0,64,1.42 +2293,,,,,,,,0, +52625,,,,,,,,0, +74588,70.0,8.0,6.0,10.0,10.0,9.0,7.0,3,0.08 +43512,95.0,10.0,10.0,10.0,10.0,9.0,9.0,90,1.97 +62239,94.0,10.0,9.0,10.0,10.0,9.0,9.0,89,1.94 +63029,96.0,9.0,10.0,9.0,10.0,10.0,9.0,14,0.47 +72902,100.0,10.0,10.0,10.0,10.0,8.0,10.0,10,0.22 +12915,86.0,9.0,9.0,9.0,9.0,9.0,9.0,46,1.0 +29262,,,,,,,,0, +9720,,,,,,,,0, +5752,99.0,10.0,9.0,10.0,10.0,10.0,10.0,18,0.4 +35664,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +56774,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.09 +10000,,,,,,,,0, +42471,97.0,10.0,10.0,10.0,10.0,9.0,10.0,31,0.69 +34287,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,0.13 +73686,100.0,10.0,10.0,10.0,10.0,10.0,10.0,38,0.84 +47856,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.66 +29694,94.0,9.0,10.0,10.0,10.0,9.0,10.0,14,0.31 +13018,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.12 +12563,65.0,8.0,5.0,9.0,9.0,9.0,7.0,4,0.09 +40697,90.0,9.0,9.0,10.0,10.0,9.0,10.0,8,0.17 +37761,100.0,9.0,9.0,10.0,10.0,10.0,9.0,2,0.05 +76456,85.0,9.0,9.0,9.0,9.0,9.0,9.0,18,0.42 +42633,95.0,10.0,10.0,10.0,10.0,10.0,9.0,27,0.64 +43444,100.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.21 +71727,,,,,,,,0, +21453,,,,,,,,0, +75829,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.07 +66490,90.0,10.0,10.0,9.0,10.0,8.0,8.0,2,0.04 +31218,90.0,9.0,10.0,10.0,10.0,8.0,10.0,2,0.05 +3180,97.0,10.0,10.0,10.0,10.0,10.0,10.0,63,1.38 +38047,99.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.41 +22160,90.0,9.0,9.0,9.0,10.0,9.0,9.0,52,1.14 +27181,96.0,10.0,9.0,10.0,10.0,9.0,10.0,18,0.41 +67836,93.0,10.0,9.0,10.0,10.0,10.0,9.0,107,2.34 +4176,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.3 +57888,,,,,,,,0, +29441,93.0,9.0,9.0,9.0,9.0,10.0,10.0,3,0.07 +28672,,,,,,,,0, +5423,95.0,10.0,10.0,10.0,10.0,9.0,9.0,41,0.92 +56458,,,,,,,,0, +48088,97.0,10.0,10.0,10.0,10.0,10.0,10.0,122,2.69 +18833,,,,,,,,0, +59686,,,,,,,,0, +69953,95.0,10.0,9.0,10.0,10.0,9.0,9.0,4,0.15 +74943,90.0,9.0,8.0,10.0,10.0,10.0,8.0,8,0.18 +3435,,,,,,,,0, +1994,95.0,10.0,10.0,10.0,10.0,10.0,9.0,8,0.17 +55126,95.0,9.0,9.0,10.0,10.0,10.0,9.0,11,0.3 +73545,77.0,9.0,7.0,9.0,9.0,9.0,8.0,6,0.14 +45459,,,,,,,,0, +30083,98.0,10.0,10.0,10.0,9.0,10.0,10.0,24,0.54 +21753,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.1 +61444,94.0,10.0,10.0,10.0,10.0,9.0,10.0,49,1.48 +23612,97.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.42 +70736,,,,,,,,0, +53769,95.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.22 +73938,97.0,10.0,10.0,10.0,10.0,10.0,10.0,62,1.44 +15230,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +53895,80.0,8.0,8.0,9.0,9.0,9.0,7.0,2,0.05 +17366,,,,,,,,0, +60064,,,,,,,,0, +46469,96.0,10.0,9.0,10.0,10.0,10.0,10.0,10,0.23 +4944,,,,,,,,0, +57507,67.0,6.0,7.0,7.0,7.0,7.0,7.0,7,0.16 +23366,99.0,10.0,10.0,10.0,10.0,10.0,10.0,39,1.11 +41280,95.0,10.0,10.0,9.0,10.0,9.0,9.0,4,0.12 +50884,100.0,10.0,9.0,9.0,10.0,9.0,10.0,3,0.07 +3906,89.0,9.0,9.0,10.0,10.0,10.0,8.0,47,1.06 +66189,97.0,10.0,10.0,10.0,10.0,10.0,10.0,26,0.68 +58476,96.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.25 +50006,88.0,9.0,9.0,10.0,10.0,10.0,9.0,199,4.38 +39199,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +29424,,,,,,,,0, +67572,75.0,9.0,8.0,9.0,10.0,10.0,9.0,7,0.44 +38027,,,,,,,,0, +52680,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.12 +10225,80.0,8.0,2.0,2.0,6.0,6.0,6.0,1,0.05 +378,99.0,10.0,10.0,10.0,10.0,10.0,10.0,31,0.71 +36456,100.0,9.0,10.0,9.0,10.0,9.0,10.0,3,3.0 +28833,,,,,,,,0, +72229,92.0,10.0,9.0,9.0,9.0,9.0,9.0,27,0.6 +37269,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.22 +40991,98.0,10.0,10.0,10.0,10.0,10.0,10.0,52,1.16 +52641,97.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.47 +58396,,,,,,,,1,0.06 +23318,95.0,10.0,9.0,10.0,10.0,9.0,10.0,82,1.81 +44656,92.0,10.0,9.0,9.0,9.0,9.0,9.0,64,1.41 +59804,99.0,10.0,10.0,10.0,10.0,9.0,10.0,22,0.55 +59501,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +59693,92.0,10.0,9.0,9.0,10.0,9.0,9.0,10,0.22 +12041,,,,,,,,0, +2849,98.0,10.0,10.0,10.0,10.0,9.0,9.0,40,1.41 +54043,92.0,9.0,9.0,10.0,9.0,10.0,9.0,79,1.82 +35382,,,,,,,,0, +15056,96.0,10.0,9.0,10.0,10.0,9.0,9.0,11,0.25 +66327,,,,,,,,0, +76220,99.0,10.0,10.0,10.0,10.0,10.0,10.0,32,0.71 +10798,,,,,,,,0, +44885,94.0,10.0,9.0,10.0,10.0,10.0,9.0,97,2.17 +39060,,,,,,,,0, +23868,99.0,10.0,10.0,10.0,10.0,10.0,10.0,43,0.96 +38734,96.0,10.0,10.0,10.0,9.0,9.0,9.0,11,0.27 +16698,99.0,10.0,10.0,10.0,10.0,10.0,10.0,35,0.79 +54242,78.0,9.0,8.0,9.0,9.0,10.0,8.0,59,1.3 +20651,86.0,9.0,8.0,9.0,9.0,10.0,8.0,7,0.35 +22328,86.0,9.0,9.0,10.0,10.0,9.0,8.0,15,0.47 +31015,85.0,8.0,9.0,9.0,9.0,9.0,8.0,56,1.24 +76802,90.0,9.0,8.0,10.0,9.0,9.0,9.0,35,0.83 +59927,94.0,10.0,9.0,10.0,10.0,9.0,9.0,17,0.43 +42121,97.0,10.0,10.0,10.0,10.0,10.0,9.0,30,0.69 +42458,93.0,10.0,10.0,9.0,9.0,9.0,9.0,7,0.16 +3343,75.0,9.0,8.0,8.0,10.0,9.0,9.0,4,0.09 +55443,,,,,,,,0, +72165,91.0,9.0,9.0,10.0,9.0,9.0,9.0,19,0.43 +41956,,,,,,,,0, +55340,,,,,,,,0, +43909,94.0,10.0,9.0,10.0,10.0,10.0,9.0,18,0.44 +20296,91.0,10.0,9.0,10.0,10.0,9.0,9.0,21,0.47 +23351,96.0,10.0,10.0,10.0,10.0,9.0,10.0,31,0.72 +758,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.04 +54719,,,,,,,,0, +4824,90.0,10.0,10.0,9.0,10.0,10.0,10.0,7,0.18 +14930,95.0,10.0,10.0,10.0,10.0,10.0,9.0,247,5.95 +575,96.0,10.0,10.0,9.0,10.0,10.0,9.0,5,0.12 +22620,88.0,9.0,9.0,9.0,10.0,10.0,9.0,5,0.14 +55698,96.0,10.0,10.0,10.0,10.0,10.0,10.0,34,1.0 +16354,98.0,10.0,10.0,10.0,10.0,10.0,10.0,70,1.6 +23795,,,,,,,,0, +24452,,,,,,,,0, +41508,80.0,8.0,7.0,10.0,10.0,10.0,10.0,2,0.05 +74348,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +47669,89.0,9.0,8.0,10.0,10.0,9.0,9.0,201,4.53 +18809,94.0,10.0,10.0,10.0,10.0,10.0,10.0,87,1.95 +74881,74.0,9.0,8.0,9.0,9.0,10.0,9.0,11,0.27 +10288,95.0,10.0,9.0,10.0,10.0,10.0,9.0,150,3.34 +8398,96.0,10.0,10.0,9.0,10.0,10.0,9.0,43,1.03 +28762,91.0,9.0,9.0,10.0,10.0,9.0,9.0,134,3.05 +9134,90.0,9.0,9.0,10.0,10.0,9.0,10.0,2,0.05 +28006,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +61929,98.0,10.0,10.0,10.0,10.0,10.0,10.0,43,0.96 +4972,95.0,10.0,10.0,10.0,10.0,10.0,10.0,36,0.86 +66277,84.0,9.0,8.0,9.0,9.0,10.0,9.0,24,0.55 +283,,,,,,,,0, +45452,94.0,10.0,9.0,10.0,10.0,10.0,9.0,31,0.69 +31681,99.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.39 +63968,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.45 +396,76.0,8.0,8.0,9.0,9.0,10.0,8.0,57,1.32 +55823,64.0,7.0,7.0,9.0,8.0,9.0,7.0,22,0.55 +41399,95.0,10.0,9.0,10.0,10.0,9.0,9.0,30,0.67 +34630,99.0,10.0,10.0,10.0,10.0,10.0,10.0,50,1.17 +9844,94.0,10.0,10.0,10.0,10.0,8.0,9.0,19,0.43 +2482,93.0,10.0,10.0,10.0,10.0,9.0,9.0,113,2.53 +55342,96.0,10.0,9.0,10.0,10.0,10.0,9.0,9,3.14 +60985,100.0,10.0,10.0,8.0,10.0,8.0,8.0,1,0.03 +16430,96.0,10.0,9.0,10.0,10.0,9.0,10.0,66,1.53 +28659,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.03 +63232,,,,,,,,0, +38462,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.12 +62525,86.0,9.0,9.0,10.0,10.0,9.0,9.0,140,3.14 +54074,98.0,10.0,10.0,10.0,10.0,10.0,9.0,32,0.77 +57427,95.0,10.0,10.0,10.0,10.0,9.0,10.0,99,2.26 +64715,98.0,10.0,10.0,10.0,10.0,10.0,9.0,10,0.28 +44182,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +51914,97.0,10.0,10.0,10.0,10.0,10.0,10.0,73,1.73 +44062,92.0,9.0,9.0,10.0,9.0,9.0,9.0,21,0.51 +65518,87.0,9.0,9.0,9.0,9.0,10.0,8.0,14,0.43 +20146,,,,,,,,0, +74115,88.0,9.0,9.0,10.0,10.0,10.0,9.0,196,4.43 +24897,,,,,,,,0, +1325,98.0,10.0,10.0,10.0,10.0,10.0,10.0,125,2.81 +21700,94.0,10.0,9.0,10.0,10.0,9.0,9.0,33,0.84 +19751,,,,,,,,0, +59682,84.0,8.0,9.0,8.0,9.0,10.0,9.0,16,0.49 +63788,65.0,7.0,8.0,7.0,6.0,8.0,7.0,11,0.3 +18369,84.0,8.0,9.0,9.0,9.0,9.0,9.0,11,0.29 +31546,,,,,,,,0, +34722,95.0,10.0,10.0,10.0,10.0,9.0,9.0,73,1.89 +48086,96.0,10.0,10.0,10.0,10.0,9.0,10.0,53,1.21 +55922,89.0,9.0,9.0,9.0,10.0,10.0,8.0,36,0.83 +51626,96.0,10.0,9.0,9.0,10.0,9.0,9.0,23,0.52 +71004,98.0,10.0,10.0,10.0,10.0,10.0,9.0,85,1.97 +29104,,,,,,,,0, +29105,99.0,10.0,10.0,10.0,10.0,10.0,10.0,124,2.87 +37049,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.3 +1175,94.0,10.0,9.0,10.0,10.0,9.0,9.0,7,0.16 +51629,100.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.1 +19982,100.0,10.0,10.0,9.0,9.0,10.0,9.0,2,0.05 +33214,,,,,,,,0, +68413,97.0,10.0,10.0,10.0,10.0,10.0,10.0,225,5.07 +70594,99.0,10.0,10.0,10.0,10.0,9.0,10.0,23,0.53 +43316,97.0,10.0,9.0,10.0,10.0,10.0,10.0,95,2.17 +58322,97.0,10.0,9.0,10.0,10.0,9.0,10.0,14,0.32 +49265,,,,,,,,0, +70697,100.0,10.0,9.0,10.0,9.0,10.0,9.0,2,0.05 +60545,100.0,10.0,10.0,10.0,10.0,8.0,10.0,4,0.09 +19040,94.0,10.0,10.0,10.0,10.0,9.0,9.0,78,1.78 +67491,94.0,10.0,10.0,10.0,10.0,10.0,10.0,57,1.31 +34222,89.0,8.0,10.0,10.0,10.0,9.0,9.0,7,0.16 +44592,91.0,9.0,9.0,10.0,9.0,9.0,9.0,12,0.27 +57426,94.0,10.0,10.0,10.0,10.0,10.0,9.0,35,0.79 +5490,97.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.14 +35094,97.0,10.0,10.0,10.0,10.0,10.0,10.0,49,1.16 +20693,,,,,,,,1,0.21 +14256,,,,,,,,0, +24968,,,,,,,,0, +23732,96.0,10.0,9.0,10.0,10.0,10.0,9.0,21,0.49 +43253,97.0,10.0,9.0,10.0,10.0,9.0,10.0,39,1.02 +56117,,,,,,,,0, +35938,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.14 +5645,98.0,10.0,10.0,10.0,10.0,10.0,10.0,45,1.19 +27400,97.0,10.0,10.0,10.0,10.0,9.0,9.0,15,0.35 +66755,97.0,10.0,10.0,10.0,9.0,10.0,10.0,6,0.9 +64440,,,,,,,,0, +54764,97.0,10.0,10.0,10.0,10.0,9.0,9.0,14,0.37 +52473,93.0,9.0,10.0,10.0,10.0,10.0,9.0,18,0.79 +23023,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +42911,96.0,10.0,9.0,10.0,10.0,10.0,9.0,26,0.73 +75058,100.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.11 +53497,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.08 +29421,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.46 +36138,,,,,,,,0, +11111,85.0,9.0,9.0,9.0,9.0,10.0,8.0,4,0.1 +71050,90.0,9.0,9.0,10.0,10.0,10.0,9.0,37,1.0 +11623,,,,,,,,0, +26287,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.78 +5875,82.0,9.0,9.0,9.0,10.0,8.0,9.0,11,0.25 +921,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.31 +3391,100.0,10.0,7.0,10.0,10.0,9.0,10.0,2,0.05 +27016,90.0,9.0,9.0,9.0,9.0,9.0,9.0,73,1.66 +10463,98.0,10.0,9.0,10.0,10.0,10.0,10.0,74,1.69 +26123,99.0,10.0,10.0,10.0,10.0,9.0,10.0,46,1.1 +59875,100.0,10.0,8.0,10.0,10.0,6.0,8.0,1,0.02 +47223,100.0,10.0,10.0,10.0,10.0,10.0,10.0,39,0.99 +45935,97.0,10.0,10.0,10.0,10.0,10.0,10.0,120,2.84 +5711,80.0,10.0,8.0,10.0,10.0,10.0,9.0,4,0.1 +75724,80.0,10.0,8.0,10.0,8.0,10.0,10.0,1,0.03 +38648,94.0,10.0,9.0,10.0,10.0,9.0,9.0,13,0.43 +41957,93.0,9.0,7.0,9.0,9.0,9.0,9.0,3,0.11 +40875,100.0,9.0,9.0,9.0,8.0,10.0,9.0,2,0.06 +18841,99.0,10.0,10.0,10.0,10.0,10.0,10.0,30,0.7 +72412,,,,,,,,0, +19099,78.0,9.0,8.0,9.0,9.0,8.0,9.0,11,0.27 +23894,80.0,8.0,6.0,10.0,10.0,8.0,6.0,1,0.09 +2604,86.0,9.0,9.0,10.0,9.0,10.0,9.0,18,0.63 +66907,97.0,10.0,10.0,10.0,10.0,10.0,10.0,28,0.65 +30325,93.0,10.0,10.0,10.0,9.0,10.0,9.0,6,0.23 +34016,97.0,10.0,10.0,10.0,10.0,9.0,10.0,34,0.8 +31464,75.0,8.0,7.0,9.0,8.0,9.0,9.0,4,0.48 +1482,88.0,9.0,9.0,10.0,10.0,9.0,9.0,29,0.78 +28508,89.0,9.0,9.0,10.0,9.0,10.0,9.0,55,1.27 +25856,,,,,,,,0, +31845,87.0,9.0,8.0,9.0,9.0,10.0,8.0,52,1.27 +26241,,,,,,,,0, +60096,84.0,9.0,8.0,10.0,10.0,9.0,9.0,18,0.45 +70030,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +42576,90.0,9.0,10.0,10.0,10.0,8.0,9.0,5,0.12 +76022,,,,,,,,0, +67600,90.0,10.0,9.0,10.0,10.0,10.0,9.0,6,0.14 +36755,99.0,10.0,10.0,10.0,10.0,10.0,10.0,38,0.88 +63569,87.0,8.0,8.0,9.0,10.0,9.0,9.0,3,0.07 +46098,100.0,9.0,9.0,10.0,10.0,10.0,9.0,3,0.07 +29100,,,,,,,,0, +18087,100.0,10.0,9.0,10.0,10.0,9.0,10.0,4,0.1 +25126,100.0,10.0,9.0,10.0,10.0,8.0,10.0,2,0.05 +59375,100.0,10.0,10.0,10.0,10.0,10.0,10.0,72,1.73 +73289,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.02 +49630,96.0,10.0,10.0,10.0,10.0,9.0,9.0,36,0.84 +62123,89.0,10.0,9.0,10.0,10.0,9.0,9.0,14,0.34 +10073,,,,,,,,0, +67434,95.0,10.0,10.0,10.0,10.0,10.0,9.0,34,0.78 +2337,94.0,10.0,10.0,10.0,10.0,9.0,10.0,129,2.94 +15097,93.0,9.0,10.0,10.0,10.0,10.0,9.0,29,0.69 +40962,93.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.07 +28462,84.0,9.0,8.0,9.0,9.0,8.0,8.0,5,0.13 +58032,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +67568,89.0,9.0,9.0,10.0,10.0,9.0,9.0,15,0.35 +71255,92.0,9.0,10.0,10.0,10.0,9.0,9.0,78,1.78 +10365,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +54495,60.0,5.0,5.0,10.0,6.0,4.0,6.0,2,0.05 +27055,85.0,9.0,8.0,9.0,10.0,10.0,9.0,16,0.38 +32443,,,,,,,,0, +75315,100.0,10.0,10.0,10.0,10.0,10.0,10.0,73,2.41 +1759,96.0,10.0,10.0,9.0,10.0,9.0,10.0,39,0.96 +7279,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.0 +70798,,,,,,,,0, +63706,96.0,10.0,10.0,10.0,10.0,10.0,9.0,45,1.06 +15155,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.27 +13119,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.05 +52888,80.0,8.0,8.0,8.0,10.0,10.0,10.0,1,0.03 +75515,98.0,9.0,10.0,9.0,10.0,9.0,10.0,13,0.31 +22000,97.0,10.0,10.0,10.0,10.0,10.0,9.0,78,1.8 +67763,,,,,,,,0, +72718,87.0,10.0,8.0,10.0,10.0,9.0,9.0,14,0.35 +35774,,,,,,,,0, +2331,,,,,,,,0, +61432,100.0,9.0,10.0,10.0,10.0,10.0,10.0,5,0.12 +69860,95.0,10.0,10.0,10.0,10.0,9.0,10.0,110,2.58 +54157,94.0,9.0,9.0,9.0,9.0,10.0,9.0,31,0.72 +43052,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.48 +63329,69.0,6.0,6.0,8.0,7.0,10.0,7.0,9,0.21 +10849,,,,,,,,0, +75482,88.0,9.0,10.0,9.0,9.0,8.0,9.0,15,0.37 +65237,98.0,10.0,10.0,10.0,10.0,10.0,10.0,85,1.96 +7854,93.0,9.0,9.0,10.0,10.0,9.0,9.0,11,0.26 +14150,100.0,10.0,10.0,10.0,10.0,9.0,10.0,8,0.18 +75900,95.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.29 +72532,93.0,10.0,9.0,10.0,10.0,10.0,9.0,28,0.65 +60492,,,,,,,,0, +48987,99.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.45 +49104,100.0,10.0,10.0,10.0,10.0,10.0,9.0,11,0.27 +65919,96.0,10.0,10.0,10.0,10.0,10.0,9.0,33,0.79 +14182,,,,,,,,0, +71374,91.0,10.0,8.0,10.0,10.0,10.0,9.0,16,0.37 +42982,,,,,,,,0, +2289,,,,,,,,0, +45871,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.03 +67066,96.0,10.0,10.0,10.0,10.0,9.0,9.0,10,0.23 +13459,82.0,9.0,9.0,10.0,9.0,9.0,9.0,62,2.23 +692,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.08 +18458,95.0,10.0,9.0,10.0,10.0,10.0,9.0,12,0.28 +58698,96.0,10.0,9.0,10.0,10.0,9.0,10.0,26,6.14 +50364,93.0,10.0,9.0,10.0,9.0,10.0,9.0,12,0.28 +29724,95.0,10.0,9.0,10.0,10.0,9.0,9.0,6,0.14 +10832,89.0,9.0,9.0,10.0,10.0,10.0,9.0,30,0.75 +58130,90.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.17 +38651,97.0,10.0,9.0,10.0,10.0,10.0,10.0,29,0.67 +4788,80.0,6.0,2.0,8.0,8.0,8.0,6.0,1,0.02 +20718,80.0,9.0,8.0,9.0,9.0,10.0,8.0,4,0.09 +66673,97.0,10.0,10.0,10.0,10.0,10.0,10.0,23,0.55 +26643,94.0,10.0,9.0,10.0,9.0,9.0,9.0,31,0.73 +38063,50.0,6.0,4.0,6.0,8.0,9.0,6.0,2,0.05 +17114,,,,,,,,0, +20596,100.0,10.0,10.0,10.0,10.0,10.0,10.0,30,0.74 +58247,80.0,8.0,6.0,9.0,9.0,9.0,8.0,2,0.05 +45338,80.0,8.0,8.0,10.0,10.0,8.0,8.0,1,0.02 +24163,97.0,10.0,10.0,10.0,10.0,10.0,9.0,93,2.19 +32303,60.0,8.0,6.0,2.0,2.0,8.0,8.0,1,0.03 +60667,,,,,,,,0, +37648,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.14 +44945,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.19 +20543,89.0,8.0,8.0,9.0,9.0,10.0,9.0,15,0.37 +28040,97.0,10.0,10.0,10.0,10.0,10.0,10.0,121,2.81 +36008,50.0,6.0,5.0,6.0,7.0,7.0,4.0,2,0.05 +35211,,,,,,,,0, +20750,80.0,8.0,10.0,8.0,10.0,10.0,8.0,1,0.02 +39310,91.0,9.0,9.0,9.0,10.0,10.0,9.0,29,0.67 +3331,93.0,9.0,9.0,9.0,10.0,9.0,9.0,9,0.21 +35607,88.0,9.0,9.0,10.0,10.0,9.0,9.0,39,0.94 +75931,82.0,9.0,8.0,9.0,9.0,9.0,9.0,121,2.93 +50975,85.0,9.0,8.0,10.0,10.0,9.0,9.0,132,3.23 +52015,98.0,10.0,10.0,10.0,10.0,9.0,9.0,8,0.2 +28371,,,,,,,,0, +44627,97.0,10.0,10.0,10.0,10.0,10.0,10.0,55,1.29 +59827,,,,,,,,0, +41316,,,,,,,,0, +54983,100.0,9.0,8.0,10.0,10.0,9.0,8.0,2,0.05 +34215,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +42483,98.0,10.0,10.0,10.0,10.0,10.0,10.0,55,3.33 +40845,,,,,,,,0, +136,,,,,,,,0, +46510,80.0,9.0,8.0,8.0,8.0,8.0,7.0,2,0.05 +30123,92.0,10.0,9.0,10.0,10.0,10.0,9.0,92,2.16 +20801,100.0,10.0,10.0,10.0,9.0,9.0,10.0,3,0.07 +58681,99.0,10.0,10.0,10.0,10.0,10.0,10.0,46,1.1 +59272,78.0,8.0,8.0,9.0,9.0,9.0,8.0,24,0.58 +59784,83.0,8.0,9.0,9.0,9.0,9.0,9.0,109,2.64 +49424,74.0,8.0,8.0,9.0,9.0,9.0,8.0,80,1.94 +16765,79.0,8.0,8.0,9.0,9.0,9.0,8.0,78,1.87 +2791,80.0,8.0,9.0,9.0,9.0,9.0,8.0,95,2.3 +36664,,,,,,,,0, +69333,99.0,10.0,9.0,10.0,10.0,10.0,10.0,26,0.99 +4596,99.0,10.0,10.0,10.0,10.0,10.0,9.0,53,1.24 +46489,100.0,8.0,10.0,10.0,10.0,8.0,10.0,1,0.02 +53742,98.0,10.0,10.0,10.0,10.0,9.0,10.0,29,0.7 +20834,97.0,10.0,10.0,10.0,10.0,9.0,10.0,162,3.79 +73365,91.0,10.0,9.0,10.0,10.0,10.0,9.0,28,0.66 +11520,89.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.27 +40027,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.05 +3975,80.0,10.0,6.0,10.0,10.0,6.0,8.0,1,0.02 +36741,93.0,10.0,9.0,10.0,10.0,10.0,9.0,25,0.62 +10776,99.0,10.0,10.0,10.0,10.0,10.0,10.0,46,1.08 +37748,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.12 +48037,100.0,10.0,8.0,10.0,10.0,8.0,10.0,1,0.02 +5003,,,,,,,,0, +44581,98.0,10.0,10.0,10.0,10.0,10.0,10.0,180,4.62 +72996,93.0,9.0,9.0,10.0,10.0,9.0,9.0,83,1.95 +66101,,,,,,,,0, +19954,83.0,9.0,8.0,9.0,9.0,8.0,8.0,98,2.3 +18656,,,,,,,,0, +46070,,,,,,,,0, +64513,,,,,,,,0, +25280,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +55428,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.02 +35507,,,,,,,,0, +21747,,,,,,,,0, +35298,70.0,10.0,10.0,8.0,10.0,10.0,10.0,2,0.05 +25172,92.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.12 +32607,100.0,10.0,10.0,9.0,10.0,10.0,10.0,5,0.15 +48684,80.0,8.0,8.0,8.0,10.0,8.0,8.0,1,0.03 +42155,91.0,9.0,9.0,10.0,10.0,9.0,9.0,16,0.38 +76027,80.0,10.0,4.0,10.0,10.0,8.0,6.0,1,0.02 +69870,,,,,,,,0, +46305,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.1 +40853,95.0,10.0,10.0,10.0,10.0,10.0,9.0,26,0.64 +49121,97.0,10.0,9.0,10.0,10.0,9.0,10.0,23,0.54 +70251,99.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.47 +60399,,,,,,,,0, +5463,100.0,10.0,8.0,8.0,8.0,8.0,10.0,1,0.02 +27179,,,,,,,,0, +42667,99.0,10.0,10.0,10.0,10.0,9.0,10.0,48,1.26 +16712,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +15550,97.0,10.0,10.0,10.0,10.0,9.0,10.0,93,2.35 +44405,98.0,10.0,10.0,10.0,10.0,10.0,10.0,158,3.69 +46119,,,,,,,,0, +17365,94.0,10.0,10.0,10.0,10.0,10.0,9.0,10,0.24 +61627,,,,,,,,0, +39968,100.0,8.0,10.0,10.0,10.0,8.0,10.0,1,0.03 +5806,,,,,,,,0, +53437,,,,,,,,0, +25793,74.0,7.0,8.0,9.0,9.0,9.0,7.0,74,1.86 +4024,99.0,10.0,10.0,10.0,10.0,9.0,10.0,20,0.69 +73042,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.12 +38245,100.0,10.0,10.0,,,,,1,0.06 +22151,,,,,,,,0, +25004,,,,,,,,0, +31582,,,,,,,,0, +51097,,,,,,,,0, +71500,100.0,10.0,10.0,9.0,10.0,10.0,10.0,2,0.05 +75764,60.0,8.0,6.0,8.0,8.0,6.0,6.0,1,0.02 +75027,85.0,9.0,7.0,10.0,10.0,10.0,9.0,4,0.09 +70503,,,,,,,,1,0.02 +25971,99.0,10.0,10.0,10.0,10.0,10.0,10.0,73,2.82 +75872,95.0,10.0,8.0,10.0,9.0,9.0,9.0,8,0.26 +74901,100.0,9.0,10.0,10.0,10.0,9.0,10.0,3,0.07 +26491,100.0,9.0,10.0,10.0,10.0,10.0,10.0,4,0.3 +9962,100.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.45 +14585,97.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.48 +10666,95.0,10.0,10.0,10.0,10.0,9.0,9.0,19,0.46 +41155,,,,,,,,0, +48158,,,,,,,,0, +76825,90.0,10.0,10.0,9.0,9.0,8.0,9.0,22,0.51 +77094,90.0,9.0,9.0,9.0,8.0,9.0,8.0,9,0.22 +44088,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,0.05 +4402,87.0,9.0,9.0,9.0,8.0,7.0,9.0,3,0.07 +23977,,,,,,,,1,0.02 +20805,,,,,,,,0, +35538,,,,,,,,0, +17535,94.0,10.0,10.0,10.0,10.0,9.0,10.0,107,2.58 +75880,100.0,9.0,10.0,10.0,10.0,9.0,10.0,6,1.73 +5051,97.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.17 +53573,92.0,9.0,9.0,10.0,9.0,10.0,9.0,43,1.01 +7825,97.0,10.0,10.0,10.0,10.0,10.0,10.0,39,0.95 +74193,91.0,9.0,9.0,10.0,10.0,10.0,9.0,32,0.76 +54770,,,,,,,,0, +8321,94.0,10.0,10.0,10.0,10.0,10.0,9.0,14,0.34 +30473,,,,,,,,0, +6523,97.0,10.0,10.0,10.0,10.0,9.0,10.0,55,1.54 +6376,,,,,,,,0, +18267,100.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.07 +62436,98.0,10.0,9.0,10.0,10.0,10.0,10.0,9,0.31 +38571,,,,,,,,0, +19026,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +20830,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +8211,94.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.68 +8144,97.0,10.0,9.0,10.0,10.0,9.0,10.0,55,1.38 +58427,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +38271,92.0,9.0,10.0,9.0,10.0,10.0,9.0,74,1.82 +72952,98.0,10.0,10.0,10.0,10.0,9.0,10.0,53,1.28 +34492,96.0,10.0,10.0,10.0,10.0,10.0,10.0,98,2.96 +1696,96.0,10.0,9.0,10.0,10.0,10.0,10.0,35,0.85 +36901,,,,,,,,0, +34872,,,,,,,,0, +38847,85.0,9.0,9.0,9.0,9.0,8.0,9.0,80,1.89 +8116,90.0,9.0,9.0,9.0,9.0,9.0,9.0,96,2.29 +14993,60.0,8.0,4.0,4.0,10.0,8.0,6.0,1,0.04 +36999,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.1 +64606,,,,,,,,0, +23878,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +66,98.0,10.0,10.0,10.0,10.0,9.0,10.0,48,1.39 +34017,94.0,10.0,9.0,10.0,10.0,10.0,9.0,15,0.37 +64645,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.07 +35388,,,,,,,,0, +62811,95.0,10.0,9.0,9.0,10.0,10.0,10.0,4,0.1 +39245,,,,,,,,0, +10889,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,0.04 +11669,,,,,,,,0, +35019,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.03 +69375,87.0,10.0,10.0,8.0,9.0,9.0,9.0,3,0.07 +45448,,,,,,,,1,0.04 +8458,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,0.02 +59466,,,,,,,,0, +35237,87.0,9.0,9.0,9.0,9.0,9.0,9.0,44,1.06 +34208,,,,,,,,0, +40148,97.0,10.0,10.0,10.0,10.0,10.0,9.0,115,2.72 +74456,86.0,9.0,9.0,9.0,9.0,8.0,9.0,20,0.47 +21611,84.0,8.0,9.0,9.0,8.0,8.0,8.0,49,1.16 +5397,90.0,9.0,9.0,9.0,9.0,9.0,10.0,2,0.05 +47851,79.0,9.0,10.0,9.0,9.0,8.0,8.0,23,0.55 +34040,,,,,,,,0, +63621,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.12 +53017,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +1108,98.0,10.0,10.0,10.0,10.0,10.0,9.0,30,2.39 +37664,,,,,,,,0, +6431,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.31 +19202,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.1 +42281,,,,,,,,0, +37752,95.0,9.0,9.0,10.0,9.0,10.0,9.0,15,0.44 +15806,95.0,10.0,9.0,10.0,10.0,9.0,9.0,16,0.39 +55430,98.0,10.0,10.0,10.0,10.0,10.0,9.0,11,0.34 +54652,,,,,,,,0, +55649,97.0,10.0,10.0,10.0,10.0,10.0,9.0,39,0.93 +3259,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +41965,95.0,10.0,9.0,10.0,10.0,10.0,10.0,5,0.12 +56119,95.0,9.0,10.0,10.0,10.0,10.0,9.0,8,0.21 +48565,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.08 +52833,,,,,,,,0, +19365,98.0,10.0,10.0,10.0,10.0,9.0,10.0,117,2.78 +32389,,,,,,,,1,0.03 +50083,80.0,8.0,8.0,10.0,10.0,8.0,8.0,1,0.02 +47325,,,,,,,,0, +22040,98.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.41 +75369,,,,,,,,0, +71188,96.0,10.0,10.0,10.0,10.0,10.0,10.0,27,0.64 +62284,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +48797,,,,,,,,0, +41460,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +22251,,,,,,,,0, +46864,97.0,10.0,10.0,10.0,10.0,9.0,9.0,6,0.35 +45581,90.0,9.0,8.0,10.0,9.0,9.0,9.0,101,2.46 +1237,89.0,9.0,9.0,10.0,10.0,9.0,9.0,86,2.09 +70676,,,,,,,,0, +65191,,,,,,,,0, +36982,100.0,10.0,6.0,10.0,10.0,10.0,10.0,2,2.0 +33250,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.02 +67041,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.12 +8952,,,,,,,,0, +27968,98.0,10.0,8.0,10.0,10.0,10.0,10.0,14,0.49 +43284,83.0,9.0,9.0,9.0,9.0,8.0,9.0,99,2.35 +368,,,,,,,,0, +38744,94.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.17 +12268,98.0,10.0,10.0,10.0,10.0,10.0,10.0,65,1.59 +54919,99.0,10.0,10.0,10.0,10.0,9.0,10.0,39,0.96 +20827,88.0,9.0,8.0,9.0,9.0,10.0,9.0,19,0.47 +29912,90.0,9.0,8.0,10.0,10.0,10.0,9.0,16,0.38 +14170,91.0,9.0,9.0,9.0,10.0,9.0,9.0,47,1.12 +67644,100.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.48 +2326,94.0,10.0,10.0,9.0,9.0,9.0,9.0,124,2.98 +65231,,,,,,,,0, +32654,97.0,10.0,10.0,10.0,10.0,8.0,10.0,6,0.15 +55594,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.14 +16070,,,,,,,,0, +49730,95.0,10.0,9.0,10.0,10.0,8.0,10.0,25,0.61 +22842,87.0,9.0,9.0,9.0,9.0,8.0,10.0,3,0.07 +53353,100.0,10.0,10.0,10.0,10.0,10.0,10.0,44,1.12 +56215,99.0,10.0,10.0,10.0,10.0,10.0,10.0,36,0.9 +7938,90.0,9.0,9.0,10.0,9.0,10.0,9.0,2,0.05 +2041,94.0,9.0,9.0,10.0,10.0,10.0,9.0,10,0.47 +70119,93.0,10.0,10.0,9.0,9.0,10.0,9.0,3,0.07 +48829,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.1 +10610,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +67446,,,,,,,,0, +70000,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.1 +72757,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.29 +58513,98.0,10.0,10.0,10.0,10.0,10.0,10.0,55,1.31 +72990,93.0,9.0,9.0,10.0,10.0,9.0,9.0,3,0.07 +24137,87.0,10.0,10.0,9.0,9.0,10.0,9.0,19,0.47 +24926,96.0,10.0,10.0,10.0,10.0,10.0,9.0,64,1.56 +32401,,,,,,,,0, +40787,,,,,,,,0, +16456,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +47546,93.0,10.0,9.0,10.0,10.0,9.0,9.0,11,0.27 +63351,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.03 +40639,90.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.12 +19505,97.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.72 +24494,88.0,9.0,9.0,10.0,10.0,10.0,9.0,9,0.22 +52354,98.0,10.0,10.0,10.0,10.0,10.0,10.0,64,1.54 +58523,92.0,10.0,9.0,10.0,10.0,10.0,10.0,13,0.33 +14978,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.1 +53352,90.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.05 +28332,100.0,9.0,8.0,10.0,10.0,10.0,10.0,7,0.22 +72695,,,,,,,,0, +47935,98.0,10.0,10.0,10.0,10.0,10.0,10.0,21,0.57 +29999,85.0,9.0,8.0,9.0,8.0,9.0,9.0,40,0.96 +36815,,,,,,,,0, +23077,94.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.17 +33136,,,,,,,,0, +22118,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.1 +17594,95.0,10.0,9.0,9.0,10.0,8.0,9.0,8,0.2 +18839,,,,,,,,0, +26159,97.0,10.0,10.0,10.0,10.0,10.0,10.0,28,0.88 +35206,,,,,,,,0, +16252,,,,,,,,0, +62481,,,,,,,,0, +5970,98.0,10.0,10.0,10.0,10.0,9.0,10.0,42,1.04 +68640,84.0,9.0,9.0,8.0,9.0,10.0,10.0,6,0.15 +17779,94.0,10.0,10.0,10.0,10.0,9.0,9.0,41,1.01 +19526,80.0,2.0,6.0,10.0,10.0,10.0,10.0,3,0.11 +11858,,,,,,,,0, +4227,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.32 +68708,99.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.6 +894,,,,,,,,0, +58847,100.0,9.0,10.0,10.0,10.0,10.0,9.0,3,0.07 +45090,90.0,10.0,8.0,10.0,10.0,8.0,9.0,4,0.1 +22006,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +43012,,,,,,,,0, +66390,91.0,9.0,9.0,10.0,10.0,10.0,9.0,19,0.45 +66721,,,,,,,,0, +65497,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +63181,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.12 +6742,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +20790,95.0,10.0,10.0,10.0,10.0,9.0,10.0,85,2.18 +8639,96.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.34 +21779,97.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.17 +38130,97.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.41 +38254,100.0,9.0,9.0,10.0,9.0,9.0,9.0,4,0.1 +66970,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.03 +27815,,,,,,,,0, +36690,95.0,10.0,9.0,10.0,10.0,8.0,10.0,6,0.15 +44049,92.0,10.0,10.0,10.0,10.0,9.0,9.0,19,0.46 +33905,,,,,,,,0, +28042,,,,,,,,0, +2274,,,,,,,,0, +69050,84.0,9.0,8.0,9.0,9.0,8.0,9.0,73,1.75 +66203,,,,,,,,0, +56388,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.05 +28575,,,,,,,,0, +2024,,,,,,,,0, +33949,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.05 +19373,96.0,10.0,10.0,10.0,10.0,10.0,9.0,136,3.27 +51106,,,,,,,,0, +35399,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.07 +10304,93.0,9.0,9.0,9.0,9.0,9.0,8.0,3,0.08 +41744,83.0,8.0,8.0,9.0,9.0,9.0,8.0,31,0.75 +70973,53.0,6.0,3.0,9.0,9.0,8.0,5.0,3,0.07 +32153,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +19192,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +51716,100.0,10.0,10.0,10.0,10.0,10.0,10.0,43,1.07 +51993,91.0,9.0,10.0,9.0,10.0,10.0,9.0,30,0.93 +54775,93.0,10.0,10.0,10.0,10.0,8.0,9.0,95,2.28 +11850,87.0,9.0,8.0,10.0,9.0,9.0,9.0,29,0.77 +14408,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.03 +4214,,,,,,,,0, +62448,90.0,9.0,9.0,10.0,10.0,9.0,9.0,38,1.11 +6382,87.0,9.0,9.0,10.0,10.0,9.0,9.0,40,1.09 +47587,60.0,4.0,7.0,8.0,6.0,7.0,6.0,3,0.08 +43763,86.0,8.0,7.0,9.0,8.0,8.0,8.0,8,0.21 +9513,80.0,10.0,8.0,8.0,10.0,8.0,8.0,2,0.05 +29576,94.0,9.0,9.0,10.0,9.0,10.0,9.0,70,1.7 +73837,,,,,,,,0, +868,98.0,10.0,10.0,10.0,10.0,9.0,10.0,90,2.41 +28100,89.0,9.0,9.0,10.0,10.0,9.0,9.0,11,0.27 +73387,88.0,9.0,8.0,9.0,10.0,10.0,10.0,12,0.29 +37675,,,,,,,,0, +69969,96.0,10.0,10.0,9.0,10.0,9.0,10.0,6,0.15 +15747,86.0,9.0,7.0,9.0,10.0,9.0,8.0,46,1.12 +26655,88.0,9.0,9.0,10.0,9.0,9.0,9.0,29,0.73 +23543,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.25 +62161,100.0,10.0,10.0,9.0,10.0,9.0,10.0,5,0.12 +37274,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.42 +32195,80.0,8.0,8.0,7.0,9.0,8.0,8.0,3,0.07 +3929,78.0,9.0,8.0,9.0,9.0,10.0,8.0,8,0.21 +54686,,,,,,,,0, +1296,91.0,9.0,9.0,10.0,10.0,10.0,9.0,9,0.25 +44580,99.0,10.0,10.0,10.0,10.0,10.0,10.0,191,4.7 +11496,91.0,9.0,9.0,9.0,10.0,10.0,9.0,31,0.8 +40909,95.0,10.0,9.0,10.0,10.0,10.0,9.0,25,0.65 +9429,100.0,10.0,10.0,10.0,10.0,10.0,6.0,1,0.03 +20117,88.0,9.0,8.0,10.0,10.0,10.0,9.0,106,2.56 +16746,,,,,,,,0, +34788,95.0,10.0,9.0,10.0,10.0,10.0,10.0,5,0.12 +29491,96.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.15 +70490,98.0,10.0,9.0,10.0,10.0,10.0,9.0,51,1.56 +7080,,,,,,,,0, +20463,93.0,10.0,9.0,10.0,10.0,9.0,10.0,48,1.17 +64219,80.0,8.0,6.0,10.0,10.0,8.0,8.0,2,0.05 +5452,,,,,,,,0, +9212,87.0,10.0,8.0,10.0,10.0,10.0,9.0,9,0.22 +18623,98.0,10.0,9.0,10.0,10.0,10.0,10.0,26,0.63 +29176,67.0,8.0,7.0,9.0,7.0,7.0,6.0,3,0.07 +44759,99.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.37 +53967,95.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.1 +9299,96.0,10.0,10.0,10.0,10.0,10.0,10.0,24,0.59 +21077,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.43 +34685,83.0,9.0,8.0,9.0,9.0,8.0,9.0,89,2.15 +29811,89.0,9.0,8.0,10.0,9.0,9.0,9.0,13,0.33 +4582,98.0,10.0,10.0,10.0,10.0,10.0,10.0,49,1.43 +27643,92.0,9.0,9.0,9.0,9.0,10.0,9.0,26,0.63 +45238,98.0,10.0,10.0,10.0,10.0,10.0,10.0,39,0.97 +6274,97.0,9.0,9.0,10.0,10.0,9.0,10.0,8,0.28 +15601,73.0,9.0,7.0,9.0,9.0,9.0,7.0,3,0.07 +2661,100.0,9.0,9.0,10.0,10.0,10.0,9.0,2,0.05 +29167,89.0,10.0,7.0,10.0,10.0,9.0,9.0,7,0.17 +35689,,,,,,,,1,0.02 +53261,98.0,10.0,10.0,10.0,10.0,10.0,10.0,79,1.93 +10194,,,,,,,,0, +48965,95.0,9.0,9.0,10.0,10.0,10.0,10.0,40,0.98 +36488,93.0,10.0,9.0,10.0,10.0,10.0,9.0,45,1.15 +20071,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.23 +18691,,,,,,,,0, +8417,96.0,10.0,9.0,10.0,10.0,10.0,10.0,10,0.25 +74763,91.0,9.0,9.0,9.0,10.0,9.0,9.0,7,0.17 +56895,96.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.15 +74311,87.0,9.0,9.0,9.0,9.0,9.0,9.0,43,1.04 +31545,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.37 +17570,83.0,8.0,9.0,9.0,9.0,9.0,9.0,95,2.32 +22604,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.13 +32369,99.0,10.0,10.0,10.0,10.0,10.0,10.0,78,1.9 +54564,,,,,,,,1,0.04 +52077,96.0,10.0,10.0,10.0,10.0,10.0,9.0,9,0.29 +58200,92.0,9.0,10.0,10.0,10.0,10.0,9.0,13,0.35 +61241,,,,,,,,0, +67166,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.38 +57800,95.0,9.0,10.0,9.0,9.0,9.0,9.0,4,0.22 +73728,93.0,10.0,9.0,9.0,10.0,9.0,9.0,9,0.22 +30541,98.0,10.0,9.0,10.0,10.0,10.0,10.0,32,0.78 +60528,80.0,9.0,8.0,9.0,9.0,8.0,8.0,8,0.19 +45121,95.0,10.0,9.0,10.0,10.0,10.0,9.0,48,1.18 +66115,100.0,8.0,10.0,10.0,9.0,10.0,10.0,2,0.05 +73535,87.0,10.0,9.0,10.0,10.0,9.0,9.0,4,0.1 +35763,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.06 +24674,93.0,10.0,10.0,10.0,10.0,9.0,9.0,75,2.26 +9093,87.0,9.0,8.0,10.0,10.0,9.0,9.0,3,0.08 +4617,90.0,10.0,9.0,10.0,10.0,9.0,9.0,21,0.54 +72109,73.0,8.0,6.0,10.0,10.0,9.0,7.0,3,0.07 +40955,98.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.28 +31458,94.0,9.0,9.0,10.0,10.0,10.0,9.0,10,0.25 +10768,100.0,10.0,10.0,8.0,10.0,8.0,8.0,1,0.03 +19716,,,,,,,,0, +28629,98.0,10.0,10.0,10.0,10.0,9.0,9.0,18,0.44 +50824,,,,,,,,0, +60860,89.0,10.0,9.0,9.0,9.0,9.0,9.0,14,0.34 +30002,83.0,9.0,9.0,9.0,9.0,9.0,8.0,233,5.68 +48261,92.0,9.0,9.0,10.0,10.0,9.0,9.0,5,0.12 +43107,97.0,10.0,10.0,10.0,10.0,9.0,9.0,7,0.18 +3617,91.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.27 +21979,96.0,10.0,10.0,10.0,10.0,10.0,10.0,120,3.05 +8724,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.02 +49083,96.0,10.0,10.0,10.0,10.0,10.0,10.0,39,1.33 +44782,,,,,,,,0, +33147,80.0,9.0,6.0,7.0,7.0,9.0,7.0,3,0.07 +60197,,,,,,,,0, +22635,,,,,,,,0, +22245,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.13 +49309,86.0,9.0,8.0,9.0,9.0,9.0,9.0,22,0.54 +64441,97.0,10.0,10.0,10.0,10.0,10.0,10.0,109,2.8 +54893,80.0,9.0,9.0,9.0,9.0,9.0,8.0,209,5.13 +65301,94.0,10.0,9.0,10.0,10.0,10.0,9.0,29,0.77 +612,99.0,10.0,10.0,10.0,10.0,10.0,10.0,29,0.75 +55113,80.0,6.0,8.0,10.0,8.0,10.0,6.0,1,0.03 +30876,,,,,,,,0, +51681,,,,,,,,0, +8536,84.0,9.0,9.0,8.0,9.0,9.0,9.0,5,0.12 +40253,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +36591,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +54030,60.0,6.0,6.0,10.0,10.0,10.0,10.0,1,0.02 +50787,99.0,10.0,10.0,10.0,10.0,10.0,9.0,16,0.51 +15903,95.0,10.0,9.0,10.0,10.0,9.0,10.0,31,0.77 +60178,,,,,,,,0, +2345,,,,,,,,0, +48885,,,,,,,,0, +12741,,,,,,,,0, +27086,96.0,10.0,10.0,10.0,10.0,9.0,10.0,88,2.14 +27778,,,,,,,,0, +60998,93.0,9.0,9.0,10.0,10.0,9.0,9.0,14,0.35 +16834,,,,,,,,0, +5143,87.0,9.0,8.0,10.0,10.0,10.0,9.0,3,0.08 +57925,93.0,9.0,9.0,10.0,10.0,9.0,9.0,64,1.59 +74329,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.03 +35454,100.0,10.0,9.0,10.0,10.0,10.0,10.0,9,0.37 +99,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.15 +22284,,,,,,,,0, +53225,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.42 +1953,98.0,10.0,10.0,10.0,10.0,9.0,10.0,65,1.62 +30644,94.0,9.0,9.0,9.0,10.0,9.0,9.0,40,1.01 +62035,70.0,7.0,8.0,9.0,9.0,7.0,7.0,2,0.05 +48738,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.1 +43001,90.0,9.0,7.0,10.0,9.0,9.0,9.0,2,0.05 +20085,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.09 +25043,90.0,9.0,9.0,10.0,10.0,9.0,9.0,35,0.87 +46018,,,,,,,,0, +66459,,,,,,,,0, +13548,,,,,,,,0, +59361,98.0,10.0,10.0,10.0,10.0,10.0,9.0,124,3.18 +68164,94.0,9.0,9.0,10.0,10.0,9.0,9.0,47,1.29 +67205,95.0,9.0,9.0,10.0,10.0,9.0,10.0,16,0.4 +28266,95.0,10.0,9.0,10.0,10.0,10.0,10.0,22,1.27 +53138,,,,,,,,0, +31041,,,,,,,,0, +54603,,,,,,,,0, +58798,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,0.18 +51479,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.87 +10761,50.0,9.0,5.0,9.0,8.0,10.0,7.0,3,0.07 +51197,100.0,10.0,9.0,10.0,10.0,9.0,10.0,2,0.05 +14698,,,,,,,,0, +45672,,,,,,,,0, +62901,,,,,,,,0, +15571,100.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.17 +45677,95.0,9.0,9.0,10.0,10.0,9.0,9.0,45,1.39 +69023,95.0,10.0,10.0,10.0,10.0,10.0,10.0,66,1.64 +4562,60.0,6.0,6.0,10.0,8.0,8.0,6.0,1,0.04 +12281,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.03 +18944,,,,,,,,0, +5641,93.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.08 +61441,97.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.24 +6839,98.0,10.0,10.0,10.0,10.0,10.0,10.0,119,2.96 +2633,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +45261,,,,,,,,0, +51113,100.0,8.0,10.0,10.0,8.0,8.0,8.0,2,0.05 +39557,100.0,9.0,9.0,10.0,10.0,10.0,10.0,4,0.1 +31932,91.0,9.0,9.0,10.0,10.0,9.0,9.0,29,0.73 +53401,95.0,10.0,9.0,10.0,10.0,10.0,10.0,12,0.3 +18975,99.0,10.0,10.0,10.0,10.0,10.0,10.0,44,1.11 +59324,,,,,,,,0, +23874,77.0,8.0,8.0,9.0,9.0,10.0,8.0,33,0.81 +75130,100.0,8.0,6.0,10.0,10.0,10.0,8.0,1,0.03 +50709,84.0,8.0,8.0,8.0,9.0,10.0,9.0,5,1.2 +60299,,,,,,,,0, +46556,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.08 +46082,,,,,,,,0, +7673,,,,,,,,0, +52988,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.14 +15853,,,,,,,,0, +61905,,,,,,,,0, +48031,79.0,8.0,8.0,9.0,9.0,10.0,7.0,24,0.71 +65917,,,,,,,,2,0.05 +74298,97.0,10.0,10.0,10.0,10.0,9.0,10.0,37,0.91 +8676,97.0,10.0,10.0,10.0,10.0,9.0,10.0,66,1.63 +45653,94.0,9.0,10.0,10.0,10.0,9.0,10.0,68,2.0 +22178,93.0,10.0,9.0,10.0,10.0,9.0,10.0,61,1.84 +49458,96.0,10.0,9.0,10.0,10.0,10.0,10.0,70,1.78 +72838,87.0,9.0,8.0,9.0,9.0,8.0,9.0,133,3.32 +24730,100.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.15 +31108,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,0.67 +65085,85.0,9.0,9.0,9.0,10.0,9.0,9.0,4,0.1 +42119,95.0,10.0,9.0,10.0,10.0,10.0,9.0,4,0.11 +37112,98.0,10.0,10.0,10.0,10.0,9.0,10.0,32,0.79 +41586,,,,,,,,0, +3843,93.0,10.0,10.0,9.0,9.0,10.0,10.0,6,0.3 +25122,85.0,8.0,8.0,9.0,9.0,10.0,9.0,16,0.41 +67219,80.0,6.0,7.0,7.0,8.0,7.0,7.0,2,0.05 +70983,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.05 +55320,,,,,,,,0, +51541,96.0,10.0,10.0,10.0,10.0,8.0,9.0,10,0.25 +62790,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +12431,,,,,,,,0, +58309,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.11 +45387,98.0,10.0,10.0,10.0,10.0,10.0,10.0,115,2.85 +66808,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.2 +61576,93.0,9.0,9.0,10.0,10.0,10.0,9.0,38,0.94 +65211,100.0,10.0,10.0,9.0,10.0,10.0,10.0,2,0.11 +22534,,,,,,,,0, +63831,93.0,10.0,10.0,10.0,9.0,7.0,9.0,3,0.08 +32348,70.0,9.0,9.0,10.0,10.0,8.0,9.0,2,0.05 +65764,95.0,9.0,9.0,9.0,10.0,10.0,10.0,4,0.1 +19349,100.0,10.0,10.0,10.0,10.0,10.0,10.0,27,0.77 +73465,,,,,,,,0, +4219,,,,,,,,0, +16797,96.0,10.0,10.0,10.0,10.0,9.0,9.0,11,0.27 +8353,,,,,,,,0, +74187,94.0,10.0,10.0,9.0,9.0,10.0,9.0,28,0.7 +69452,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +29099,,,,,,,,0, +30319,91.0,10.0,10.0,10.0,9.0,10.0,9.0,15,0.39 +25033,93.0,10.0,9.0,10.0,10.0,10.0,9.0,8,0.21 +43902,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.15 +50111,20.0,2.0,2.0,2.0,2.0,2.0,2.0,2,0.05 +14901,,,,,,,,0, +25187,85.0,9.0,10.0,8.0,9.0,10.0,9.0,18,2.44 +59442,,,,,,,,0, +30729,,,,,,,,0, +42686,99.0,10.0,10.0,10.0,10.0,9.0,9.0,18,0.47 +61222,99.0,10.0,10.0,10.0,10.0,10.0,10.0,41,1.05 +68855,96.0,10.0,10.0,10.0,10.0,10.0,9.0,20,0.5 +38021,,,,,,,,0, +1047,92.0,9.0,9.0,10.0,10.0,9.0,9.0,58,1.49 +52358,98.0,10.0,10.0,10.0,10.0,9.0,10.0,24,0.6 +3773,94.0,10.0,9.0,10.0,10.0,9.0,9.0,16,0.41 +48247,100.0,8.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +37031,80.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.05 +42089,90.0,10.0,9.0,10.0,10.0,9.0,9.0,10,0.33 +40093,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.34 +38705,,,,,,,,0, +24244,,,,,,,,0, +75414,,,,,,,,0, +20185,87.0,9.0,9.0,9.0,9.0,9.0,8.0,12,0.33 +4738,92.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.33 +15556,96.0,10.0,9.0,10.0,10.0,8.0,10.0,10,0.25 +51235,100.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.14 +76739,,,,,,,,0, +54160,80.0,7.0,8.0,8.0,10.0,9.0,9.0,3,0.08 +3844,86.0,9.0,9.0,10.0,10.0,10.0,9.0,30,0.82 +7505,,,,,,,,0, +10295,80.0,9.0,8.0,10.0,10.0,9.0,8.0,57,3.59 +16061,83.0,9.0,8.0,9.0,10.0,10.0,9.0,38,2.51 +62078,83.0,9.0,9.0,9.0,9.0,9.0,9.0,66,4.09 +5293,98.0,10.0,10.0,10.0,10.0,10.0,10.0,51,1.26 +41097,,,,,,,,1,0.03 +55115,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.03 +28308,96.0,10.0,10.0,10.0,10.0,10.0,9.0,120,2.97 +70737,97.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.37 +49887,,,,,,,,0, +21774,98.0,10.0,10.0,10.0,10.0,9.0,10.0,25,0.83 +16453,92.0,10.0,10.0,10.0,9.0,9.0,9.0,5,0.14 +63500,87.0,8.0,9.0,10.0,10.0,10.0,8.0,75,1.87 +53372,,,,,,,,0, +3246,,,,,,,,0, +68510,97.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.36 +55853,98.0,10.0,10.0,10.0,10.0,9.0,9.0,8,0.21 +11861,99.0,10.0,10.0,10.0,10.0,10.0,9.0,16,0.45 +53203,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +71764,,,,,,,,0, +55348,90.0,9.0,8.0,9.0,9.0,9.0,8.0,4,0.1 +3656,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.5 +14995,93.0,9.0,8.0,10.0,10.0,8.0,9.0,6,0.35 +45237,79.0,8.0,9.0,8.0,8.0,9.0,8.0,16,0.43 +21151,,,,,,,,0, +1439,,,,,,,,0, +14486,87.0,9.0,9.0,10.0,9.0,9.0,8.0,3,0.08 +50675,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.04 +53541,96.0,10.0,10.0,10.0,10.0,9.0,9.0,10,0.25 +16377,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,5.13 +23622,,,,,,,,0, +37310,,,,,,,,0, +44459,80.0,8.0,6.0,8.0,8.0,8.0,8.0,1,0.03 +65455,,,,,,,,0, +18198,,,,,,,,0, +11955,90.0,10.0,9.0,10.0,10.0,10.0,10.0,8,0.27 +65888,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.22 +9359,97.0,10.0,10.0,10.0,10.0,10.0,9.0,52,1.4 +15794,,,,,,,,0, +66616,100.0,,,,10.0,10.0,10.0,1,0.03 +8044,,,,,,,,1,0.03 +19990,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.02 +66247,80.0,10.0,8.0,10.0,8.0,10.0,8.0,1,0.03 +4080,80.0,8.0,6.0,10.0,6.0,10.0,8.0,1,0.03 +69590,100.0,9.0,9.0,9.0,10.0,10.0,9.0,3,3.0 +1416,75.0,8.0,7.0,10.0,10.0,10.0,7.0,4,0.1 +58170,93.0,10.0,9.0,10.0,10.0,9.0,10.0,9,0.23 +8553,,,,,,,,0, +39385,,,,,,,,1,0.03 +8745,80.0,8.0,8.0,10.0,10.0,10.0,10.0,1,0.02 +52008,95.0,10.0,10.0,10.0,10.0,10.0,10.0,138,3.47 +68141,96.0,10.0,9.0,10.0,10.0,10.0,9.0,14,0.36 +6688,,,,,,,,0, +21287,,,,,,,,0, +28553,89.0,9.0,10.0,10.0,10.0,9.0,9.0,11,0.28 +14375,97.0,10.0,10.0,10.0,10.0,10.0,10.0,41,1.03 +40488,96.0,9.0,9.0,10.0,10.0,10.0,9.0,20,0.51 +59137,100.0,10.0,9.0,10.0,10.0,10.0,9.0,8,0.32 +26941,100.0,10.0,8.0,8.0,10.0,10.0,8.0,1,0.03 +46705,96.0,10.0,9.0,10.0,10.0,9.0,9.0,19,0.48 +76201,,,,,,,,0, +38570,98.0,10.0,10.0,10.0,10.0,9.0,10.0,49,1.24 +995,96.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.48 +56417,93.0,9.0,9.0,10.0,10.0,10.0,9.0,47,1.18 +50917,95.0,10.0,10.0,10.0,10.0,9.0,10.0,13,6.19 +47510,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +45408,95.0,10.0,9.0,10.0,10.0,10.0,9.0,39,1.02 +41575,80.0,6.0,10.0,10.0,10.0,8.0,10.0,1,0.03 +44738,,,,,,,,0, +64571,95.0,10.0,10.0,10.0,10.0,10.0,8.0,4,0.75 +38522,96.0,10.0,9.0,10.0,10.0,10.0,10.0,10,0.25 +31769,90.0,9.0,7.0,9.0,9.0,10.0,9.0,2,0.05 +59019,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.34 +62376,95.0,10.0,9.0,10.0,10.0,10.0,9.0,43,1.11 +26385,93.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.08 +55851,,,,,,,,0, +58857,97.0,10.0,10.0,10.0,10.0,9.0,10.0,43,1.15 +33428,,,,,,,,0, +51897,98.0,10.0,10.0,10.0,10.0,10.0,10.0,83,2.07 +7126,,,,,,,,0, +73462,100.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.39 +11564,,,,,,,,0, +11251,93.0,10.0,9.0,10.0,10.0,9.0,9.0,83,2.11 +42234,88.0,10.0,9.0,10.0,10.0,9.0,9.0,8,0.41 +26525,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.21 +22788,87.0,9.0,8.0,9.0,10.0,8.0,8.0,4,0.1 +6297,80.0,9.0,8.0,9.0,9.0,10.0,8.0,119,3.04 +34644,82.0,9.0,8.0,10.0,9.0,9.0,8.0,46,1.18 +45179,96.0,10.0,9.0,10.0,10.0,9.0,9.0,12,0.3 +22828,,,,,,,,0, +77090,,,,,,,,0, +69814,,,,,,,,1,0.03 +35634,93.0,10.0,9.0,10.0,10.0,9.0,10.0,25,0.64 +8711,86.0,9.0,9.0,9.0,8.0,8.0,9.0,17,0.43 +9197,90.0,9.0,9.0,9.0,10.0,10.0,9.0,14,0.36 +1816,87.0,9.0,9.0,9.0,9.0,10.0,8.0,31,0.79 +63132,96.0,9.0,10.0,10.0,10.0,10.0,9.0,26,0.9 +19183,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +26026,,,,,,,,0, +76420,96.0,10.0,10.0,10.0,10.0,10.0,9.0,12,0.31 +15870,93.0,10.0,9.0,10.0,10.0,10.0,9.0,6,0.15 +18952,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.15 +27764,90.0,10.0,10.0,10.0,10.0,8.0,10.0,16,0.41 +51971,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.36 +66699,94.0,10.0,9.0,10.0,10.0,9.0,10.0,10,0.26 +15307,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.31 +67910,96.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.17 +32026,99.0,10.0,10.0,10.0,10.0,9.0,10.0,19,0.65 +53819,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +41513,,,,,,,,0, +23599,93.0,10.0,9.0,10.0,10.0,9.0,9.0,15,0.38 +6920,80.0,7.0,9.0,9.0,9.0,8.0,9.0,3,0.1 +37359,90.0,9.0,9.0,10.0,10.0,10.0,9.0,4,0.1 +29107,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.03 +43582,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.15 +3351,91.0,9.0,9.0,10.0,10.0,10.0,9.0,63,1.6 +35585,80.0,8.0,8.0,9.0,9.0,9.0,8.0,9,0.23 +65043,98.0,10.0,10.0,10.0,10.0,10.0,10.0,281,7.08 +26060,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.13 +11912,80.0,10.0,8.0,10.0,10.0,2.0,10.0,1,0.03 +75288,96.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.23 +32623,76.0,8.0,7.0,9.0,9.0,10.0,8.0,15,0.38 +58138,94.0,9.0,10.0,10.0,10.0,9.0,10.0,40,1.02 +28617,100.0,10.0,8.0,10.0,10.0,8.0,10.0,1,0.03 +47654,,,,,,,,0, +68943,97.0,10.0,10.0,10.0,10.0,10.0,10.0,69,1.76 +32616,96.0,10.0,10.0,10.0,10.0,10.0,10.0,299,7.58 +58199,100.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.18 +75703,98.0,10.0,10.0,10.0,10.0,10.0,10.0,60,1.56 +55419,92.0,10.0,9.0,10.0,10.0,10.0,9.0,6,0.15 +49381,98.0,10.0,10.0,10.0,10.0,9.0,10.0,48,1.22 +52365,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.42 +54406,99.0,10.0,10.0,10.0,10.0,9.0,10.0,82,2.45 +48090,,,,,,,,0, +18963,97.0,10.0,10.0,10.0,10.0,9.0,10.0,139,3.53 +55172,90.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.05 +1611,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +68645,98.0,10.0,10.0,10.0,10.0,10.0,10.0,53,1.4 +48308,93.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.1 +37264,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.41 +45746,,,,,,,,0, +8753,,,,,,,,0, +21058,,,,,,,,0, +25819,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.13 +28941,97.0,10.0,10.0,10.0,10.0,10.0,10.0,23,0.7 +17911,90.0,10.0,8.0,10.0,9.0,10.0,9.0,2,0.05 +49116,,,,,,,,0, +63431,,,,,,,,1,0.03 +50993,,,,,,,,0, +76140,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.08 +4512,76.0,9.0,8.0,9.0,9.0,8.0,8.0,5,0.13 +218,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +1206,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.13 +73177,,,,,,,,0, +28706,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.28 +43147,,,,,,,,0, +35981,89.0,9.0,9.0,9.0,10.0,10.0,9.0,35,0.9 +35040,,,,,,,,0, +8590,,,,,,,,0, +19428,91.0,9.0,9.0,9.0,9.0,10.0,8.0,11,0.28 +33573,94.0,9.0,9.0,10.0,10.0,9.0,10.0,29,0.74 +9654,80.0,7.0,6.0,10.0,10.0,9.0,9.0,3,0.08 +27250,,,,,,,,1,0.21 +61603,87.0,9.0,9.0,9.0,10.0,9.0,9.0,3,0.08 +63464,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.15 +24803,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +34830,71.0,8.0,8.0,10.0,9.0,8.0,7.0,7,0.18 +54555,,,,,,,,0, +13838,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.11 +19513,90.0,9.0,10.0,10.0,10.0,9.0,9.0,4,0.14 +67457,,,,,,,,0, +53412,94.0,9.0,10.0,10.0,10.0,10.0,10.0,17,0.43 +44781,95.0,10.0,9.0,10.0,10.0,10.0,9.0,23,0.59 +70793,,,,,,,,0, +23433,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +51265,,,,,,,,1,0.03 +51949,,,,,,,,0, +74959,,,,,,,,0, +72608,,,,,,,,0, +54842,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.1 +6881,,,,,,,,0, +14788,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.1 +42374,95.0,10.0,10.0,10.0,10.0,9.0,9.0,222,5.65 +55458,92.0,10.0,10.0,10.0,10.0,9.0,9.0,194,4.94 +50424,98.0,10.0,10.0,10.0,10.0,10.0,10.0,55,1.4 +65474,100.0,6.0,8.0,10.0,8.0,6.0,8.0,1,0.03 +47460,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.21 +34145,,,,,,,,0, +18213,84.0,9.0,10.0,10.0,10.0,10.0,10.0,5,0.17 +61231,89.0,9.0,10.0,9.0,9.0,10.0,9.0,137,3.87 +56115,95.0,10.0,9.0,10.0,10.0,10.0,9.0,37,0.95 +62190,,,,,,,,0, +16267,60.0,10.0,4.0,4.0,6.0,6.0,6.0,1,0.03 +36596,87.0,9.0,8.0,10.0,9.0,9.0,9.0,4,0.14 +23722,93.0,9.0,9.0,10.0,10.0,10.0,10.0,59,2.39 +34504,90.0,9.0,9.0,9.0,9.0,9.0,9.0,2,0.05 +29675,98.0,10.0,10.0,10.0,10.0,10.0,9.0,9,0.28 +43118,98.0,10.0,10.0,10.0,10.0,10.0,9.0,94,2.39 +67656,,,,,,,,0, +52543,95.0,10.0,9.0,10.0,10.0,10.0,9.0,17,0.6 +25723,93.0,10.0,9.0,10.0,10.0,10.0,9.0,6,0.15 +24144,,,,,,,,0, +55722,,,,,,,,0, +38453,98.0,10.0,10.0,10.0,10.0,9.0,10.0,66,1.68 +46473,,,,,,,,0, +37823,86.0,9.0,9.0,9.0,10.0,8.0,9.0,8,0.21 +25314,93.0,8.0,9.0,10.0,10.0,10.0,9.0,9,0.23 +37077,,,,,,,,0, +59653,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +63766,50.0,7.0,6.0,8.0,10.0,8.0,4.0,2,0.07 +63151,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +6180,96.0,10.0,10.0,9.0,9.0,10.0,9.0,14,0.36 +24560,95.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.21 +74276,94.0,10.0,10.0,10.0,10.0,10.0,9.0,23,0.59 +70474,91.0,10.0,10.0,10.0,9.0,10.0,9.0,18,0.46 +30923,100.0,10.0,10.0,10.0,10.0,9.0,10.0,10,0.26 +44758,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +3846,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +68945,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,0.05 +49622,94.0,9.0,9.0,9.0,9.0,8.0,9.0,7,0.18 +32662,,,,,,,,0, +4060,90.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.05 +58784,,,,,,,,0, +23590,100.0,10.0,10.0,8.0,10.0,8.0,10.0,2,0.05 +58812,,,,,,,,1,0.17 +54898,97.0,10.0,9.0,10.0,10.0,9.0,10.0,116,3.0 +75910,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +72721,,,,,,,,0, +7987,100.0,10.0,10.0,9.0,10.0,10.0,9.0,23,1.87 +46910,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.1 +23989,,,,,,,,0, +19376,93.0,10.0,9.0,10.0,10.0,9.0,9.0,56,1.69 +16159,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.4 +19272,,,,,,,,1,0.03 +1646,88.0,9.0,8.0,10.0,10.0,8.0,9.0,9,0.23 +639,60.0,10.0,6.0,10.0,10.0,10.0,8.0,1,0.03 +70413,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +20428,92.0,10.0,9.0,9.0,9.0,10.0,9.0,5,0.14 +21854,93.0,9.0,9.0,9.0,10.0,9.0,10.0,26,0.67 +1650,100.0,8.0,8.0,8.0,10.0,8.0,8.0,1,0.03 +52439,,,,,,,,0, +11618,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.08 +31892,,,,,,,,0, +59307,,,,,,,,0, +52633,80.0,8.0,8.0,10.0,10.0,8.0,8.0,1,0.03 +27621,92.0,9.0,10.0,10.0,10.0,10.0,9.0,13,0.34 +6606,96.0,10.0,10.0,10.0,10.0,9.0,10.0,131,3.35 +66005,,,,,,,,0, +45756,97.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.17 +26202,95.0,10.0,9.0,10.0,10.0,9.0,10.0,13,0.35 +54119,93.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.38 +3533,88.0,9.0,9.0,9.0,9.0,8.0,9.0,58,1.48 +54989,,,,,,,,0, +56785,80.0,9.0,9.0,9.0,9.0,9.0,8.0,2,0.05 +36171,,,,,,,,0, +53038,91.0,9.0,9.0,9.0,10.0,9.0,9.0,124,3.21 +31105,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.13 +1598,97.0,10.0,9.0,10.0,10.0,10.0,10.0,64,1.79 +73560,100.0,10.0,10.0,10.0,10.0,10.0,10.0,88,2.31 +33360,80.0,8.0,7.0,9.0,9.0,10.0,8.0,11,0.29 +54309,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.24 +38265,99.0,10.0,10.0,10.0,10.0,9.0,10.0,68,1.75 +38433,,,,,,,,0, +5951,95.0,9.0,9.0,10.0,10.0,10.0,9.0,41,1.09 +25654,,,,,,,,0, +17845,94.0,10.0,10.0,10.0,10.0,9.0,9.0,50,1.29 +66494,97.0,10.0,10.0,10.0,10.0,10.0,10.0,66,3.54 +10465,96.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.16 +22776,20.0,,,,2.0,,,2,0.05 +23847,99.0,10.0,10.0,10.0,10.0,10.0,10.0,61,1.57 +4937,96.0,10.0,10.0,10.0,10.0,9.0,10.0,63,1.64 +56695,98.0,10.0,10.0,10.0,10.0,10.0,10.0,29,0.77 +71717,,,,,,,,0, +59342,97.0,10.0,10.0,10.0,10.0,10.0,10.0,66,2.03 +3747,80.0,7.0,8.0,10.0,9.0,10.0,9.0,6,0.15 +66419,98.0,10.0,10.0,10.0,10.0,9.0,10.0,26,0.67 +25495,,,,,,,,0, +1665,96.0,10.0,9.0,9.0,10.0,9.0,9.0,17,0.44 +56242,97.0,10.0,10.0,10.0,10.0,9.0,9.0,36,1.82 +29164,95.0,10.0,9.0,10.0,10.0,9.0,9.0,48,1.25 +54911,98.0,10.0,9.0,10.0,10.0,10.0,10.0,8,0.32 +26360,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.41 +7605,,,,,,,,0, +24226,99.0,10.0,10.0,10.0,10.0,10.0,10.0,31,0.8 +53230,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +70375,60.0,10.0,2.0,10.0,10.0,10.0,4.0,1,0.29 +75768,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.29 +36513,,,,,,,,0, +15802,,,,,,,,0, +23770,98.0,10.0,10.0,10.0,10.0,9.0,10.0,76,2.01 +45358,93.0,10.0,9.0,10.0,10.0,9.0,9.0,18,0.48 +61289,94.0,10.0,10.0,9.0,10.0,9.0,10.0,9,0.24 +70585,,,,,,,,1,0.03 +72022,84.0,9.0,9.0,9.0,10.0,8.0,8.0,73,1.89 +45560,89.0,9.0,8.0,9.0,9.0,9.0,9.0,17,0.45 +43095,89.0,9.0,9.0,10.0,10.0,10.0,9.0,33,0.86 +41741,,,,,,,,0, +11679,,,,,,,,0, +2457,87.0,9.0,9.0,9.0,10.0,10.0,9.0,44,1.15 +12230,97.0,10.0,10.0,10.0,10.0,10.0,9.0,8,0.21 +17479,,,,,,,,0, +25555,73.0,7.0,9.0,9.0,8.0,9.0,6.0,6,0.16 +68550,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +45472,,,,,,,,0, +27866,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.13 +12636,,,,,,,,0, +34679,,,,,,,,0, +14948,,,,,,,,0, +43125,,,,,,,,0, +1117,,,,,,,,0, +45162,90.0,10.0,10.0,9.0,10.0,10.0,10.0,2,0.05 +44693,64.0,7.0,8.0,8.0,8.0,9.0,7.0,10,0.26 +5613,,,,,,,,0, +2154,98.0,10.0,10.0,9.0,10.0,9.0,9.0,8,0.21 +40250,97.0,10.0,10.0,9.0,10.0,9.0,10.0,19,0.55 +20396,91.0,10.0,10.0,10.0,10.0,9.0,10.0,157,4.08 +4008,64.0,7.0,6.0,6.0,7.0,8.0,7.0,6,0.17 +63654,,,,,,,,0, +30833,94.0,10.0,10.0,10.0,10.0,10.0,9.0,74,1.95 +60305,85.0,9.0,8.0,9.0,9.0,10.0,9.0,122,3.22 +45281,87.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.08 +69365,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.17 +52071,96.0,10.0,10.0,10.0,10.0,9.0,10.0,42,1.09 +26513,100.0,10.0,9.0,9.0,9.0,9.0,9.0,3,0.08 +47603,94.0,9.0,9.0,10.0,10.0,9.0,10.0,29,5.96 +67863,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.57 +53510,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +66811,96.0,10.0,10.0,10.0,10.0,9.0,10.0,79,2.09 +71194,94.0,10.0,10.0,10.0,10.0,10.0,10.0,40,1.08 +40406,94.0,10.0,9.0,10.0,10.0,8.0,9.0,7,0.18 +66049,100.0,9.0,8.0,9.0,9.0,9.0,8.0,2,0.06 +40216,96.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.32 +34662,90.0,9.0,8.0,10.0,10.0,9.0,9.0,23,0.6 +56963,,,,,,,,0, +33646,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.22 +47705,80.0,7.0,8.0,10.0,9.0,10.0,8.0,6,0.89 +29896,,,,,,,,0, +45273,,,,,,,,0, +13747,,,,,,,,0, +74587,95.0,10.0,10.0,10.0,10.0,10.0,9.0,44,1.15 +22717,60.0,4.0,4.0,10.0,8.0,10.0,6.0,1,0.03 +13454,,,,,,,,0, +42821,93.0,10.0,10.0,10.0,9.0,10.0,9.0,4,0.11 +35509,,,,,,,,1,0.03 +57517,91.0,9.0,10.0,10.0,9.0,9.0,9.0,203,5.31 +31461,90.0,9.0,10.0,10.0,10.0,9.0,9.0,223,5.82 +27794,98.0,10.0,10.0,10.0,10.0,10.0,10.0,71,1.99 +49436,91.0,9.0,9.0,9.0,9.0,9.0,9.0,8,0.21 +74848,83.0,9.0,8.0,10.0,10.0,9.0,8.0,152,3.97 +16907,97.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.19 +9270,78.0,8.0,8.0,9.0,9.0,9.0,8.0,134,3.53 +48878,40.0,8.0,4.0,10.0,10.0,6.0,4.0,1,0.03 +30916,,,,,,,,0, +3371,,,,,,,,0, +42067,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +54469,95.0,10.0,10.0,10.0,10.0,9.0,9.0,117,3.04 +3961,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +65439,97.0,10.0,10.0,10.0,10.0,9.0,10.0,25,0.8 +42279,,,,,,,,0, +21666,100.0,10.0,10.0,10.0,10.0,8.0,9.0,4,0.17 +16056,97.0,10.0,9.0,10.0,10.0,10.0,9.0,49,2.32 +31495,,,,,,,,0, +9929,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.43 +32844,98.0,10.0,10.0,10.0,10.0,10.0,10.0,24,0.65 +23255,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.05 +21605,99.0,10.0,10.0,10.0,10.0,10.0,10.0,55,1.44 +15952,98.0,10.0,10.0,10.0,10.0,9.0,10.0,29,0.76 +2591,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.54 +30422,60.0,8.0,4.0,8.0,8.0,8.0,6.0,1,0.03 +59256,90.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.1 +54130,,,,,,,,0, +41169,,,,,,,,0, +3079,89.0,9.0,9.0,9.0,10.0,10.0,9.0,59,1.54 +15189,95.0,10.0,10.0,10.0,9.0,10.0,10.0,5,0.21 +6473,,,,,,,,0, +6518,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.05 +9473,99.0,10.0,10.0,10.0,10.0,10.0,10.0,64,2.35 +7139,93.0,9.0,8.0,9.0,10.0,10.0,9.0,8,0.21 +15216,95.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.51 +74288,,,,,,,,0, +28504,,,,,,,,0, +18604,91.0,9.0,9.0,10.0,10.0,9.0,9.0,38,1.04 +35390,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.05 +56384,94.0,10.0,9.0,10.0,10.0,10.0,10.0,13,2.34 +8202,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.34 +25530,73.0,9.0,7.0,10.0,10.0,9.0,9.0,4,0.11 +66979,90.0,9.0,9.0,10.0,9.0,9.0,9.0,23,0.61 +42210,,,,,,,,0, +35713,100.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.12 +19855,97.0,10.0,9.0,10.0,10.0,10.0,9.0,35,0.99 +30992,97.0,10.0,10.0,10.0,10.0,9.0,9.0,12,0.33 +21415,100.0,10.0,10.0,10.0,10.0,10.0,9.0,11,0.29 +28254,90.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.09 +30028,87.0,9.0,9.0,10.0,9.0,9.0,9.0,14,0.39 +21069,97.0,10.0,9.0,10.0,10.0,9.0,10.0,38,1.01 +36021,,,,,,,,0, +37218,86.0,9.0,9.0,10.0,10.0,10.0,9.0,10,0.36 +20815,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.51 +64923,97.0,10.0,10.0,10.0,10.0,10.0,10.0,35,0.94 +73104,,,,,,,,0, +11081,93.0,9.0,9.0,10.0,10.0,10.0,9.0,49,1.29 +5271,98.0,10.0,9.0,10.0,10.0,9.0,10.0,10,0.75 +13669,96.0,10.0,9.0,10.0,10.0,10.0,10.0,33,2.41 +21239,90.0,10.0,8.0,9.0,10.0,9.0,9.0,8,0.22 +39947,,,,,,,,0, +54598,,,,,,,,1,0.03 +10404,97.0,10.0,10.0,10.0,10.0,10.0,9.0,37,2.19 +23,,,,,,,,0, +22278,,,,,,,,0, +70132,99.0,10.0,10.0,10.0,10.0,10.0,10.0,80,2.19 +73356,99.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.49 +35095,96.0,10.0,9.0,10.0,10.0,9.0,10.0,20,0.55 +19807,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +27655,97.0,10.0,10.0,10.0,10.0,10.0,10.0,187,4.95 +77049,87.0,9.0,9.0,10.0,10.0,9.0,9.0,3,0.08 +36887,93.0,10.0,10.0,10.0,10.0,10.0,9.0,83,6.62 +64245,93.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.2 +35944,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.45 +46243,89.0,9.0,9.0,9.0,10.0,10.0,9.0,12,0.34 +45894,87.0,8.0,7.0,9.0,10.0,9.0,7.0,3,0.08 +38666,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.21 +43943,91.0,9.0,9.0,10.0,9.0,9.0,10.0,7,0.18 +52578,79.0,8.0,8.0,10.0,10.0,10.0,8.0,21,0.59 +73718,,,,,,,,0, +24408,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +35328,96.0,10.0,10.0,10.0,10.0,10.0,10.0,81,2.16 +33097,88.0,9.0,8.0,10.0,9.0,9.0,9.0,16,0.93 +34199,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.11 +63884,94.0,9.0,10.0,9.0,10.0,10.0,9.0,14,0.37 +56548,90.0,9.0,9.0,10.0,10.0,9.0,9.0,2,0.05 +56887,,,,,,,,0, +51843,86.0,10.0,10.0,9.0,9.0,9.0,9.0,8,0.24 +64846,93.0,9.0,9.0,10.0,10.0,10.0,10.0,16,0.44 +41791,94.0,10.0,10.0,10.0,10.0,10.0,10.0,53,1.4 +55164,96.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.46 +68869,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.53 +49448,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +22559,93.0,9.0,10.0,9.0,10.0,10.0,10.0,3,0.12 +34473,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +27939,93.0,9.0,9.0,9.0,9.0,10.0,9.0,37,1.04 +58433,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.25 +62242,,,,,,,,0, +58509,90.0,10.0,10.0,9.0,10.0,9.0,10.0,2,0.05 +10345,94.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.19 +64973,,,,,,,,0, +14281,,,,,,,,0, +4796,92.0,9.0,9.0,10.0,10.0,9.0,9.0,51,1.69 +18165,,,,,,,,0, +67449,80.0,9.0,7.0,10.0,10.0,10.0,8.0,3,0.08 +28543,95.0,10.0,8.0,9.0,9.0,10.0,9.0,9,0.27 +69887,99.0,10.0,10.0,10.0,10.0,10.0,10.0,81,2.15 +57446,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.34 +69126,93.0,10.0,9.0,10.0,10.0,10.0,10.0,9,0.24 +38087,98.0,10.0,10.0,10.0,10.0,9.0,10.0,24,0.67 +56638,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +32545,,,,,,,,0, +51774,100.0,9.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +40741,80.0,6.0,7.0,10.0,10.0,10.0,7.0,3,0.08 +38653,,,,,,,,0, +29564,,,,,,,,0, +58820,,,,,,,,0, +76418,90.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.06 +71450,,,,,,,,0, +76473,80.0,8.0,8.0,9.0,9.0,9.0,9.0,43,1.19 +56445,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.65 +70706,91.0,10.0,9.0,10.0,10.0,9.0,9.0,29,0.78 +1289,,,,,,,,0, +65825,,,,,,,,0, +7658,82.0,9.0,8.0,9.0,9.0,9.0,8.0,106,2.82 +10022,86.0,9.0,9.0,9.0,9.0,10.0,9.0,119,3.14 +20821,99.0,10.0,10.0,10.0,10.0,10.0,10.0,38,1.02 +70995,96.0,10.0,10.0,9.0,10.0,10.0,9.0,11,0.29 +7163,93.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.11 +5387,,,,,,,,0, +34418,80.0,8.0,8.0,9.0,9.0,9.0,8.0,166,4.43 +54259,,,,,,,,1,0.03 +69642,82.0,7.0,9.0,9.0,10.0,9.0,8.0,10,0.29 +35212,86.0,9.0,8.0,9.0,10.0,9.0,8.0,13,0.35 +52003,95.0,10.0,10.0,10.0,10.0,9.0,9.0,24,0.7 +56619,100.0,,10.0,,10.0,,,1,0.03 +71855,,,,,,,,0, +7822,94.0,9.0,10.0,10.0,10.0,10.0,9.0,52,1.46 +65343,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.22 +50358,97.0,10.0,10.0,10.0,10.0,9.0,9.0,63,1.72 +50078,,,,,,,,3,0.08 +32561,100.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.03 +64592,,,,,,,,1,0.07 +17031,82.0,9.0,8.0,9.0,8.0,8.0,8.0,18,0.48 +43817,,,,,,,,0, +70234,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.3 +34921,88.0,10.0,8.0,10.0,10.0,9.0,9.0,5,0.14 +26498,96.0,10.0,9.0,10.0,10.0,10.0,9.0,162,4.29 +18271,83.0,9.0,7.0,9.0,9.0,9.0,8.0,37,0.99 +73671,85.0,9.0,8.0,9.0,9.0,10.0,9.0,145,3.85 +42344,100.0,10.0,9.0,10.0,10.0,10.0,10.0,5,0.18 +6536,60.0,5.0,6.0,5.0,7.0,9.0,6.0,4,0.12 +47153,96.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.29 +41600,,,,,,,,1,0.03 +26801,94.0,10.0,9.0,10.0,10.0,10.0,9.0,42,1.12 +50524,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.34 +31740,,,,,,,,0, +4147,97.0,10.0,10.0,10.0,10.0,9.0,10.0,55,1.47 +45789,84.0,8.0,9.0,9.0,9.0,10.0,8.0,11,0.3 +29958,100.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.69 +18201,94.0,10.0,9.0,10.0,10.0,9.0,10.0,82,2.2 +52490,96.0,10.0,10.0,10.0,10.0,10.0,9.0,21,0.59 +57122,85.0,9.0,9.0,9.0,10.0,10.0,10.0,4,0.11 +66434,89.0,9.0,9.0,10.0,10.0,9.0,9.0,76,2.06 +17285,90.0,10.0,9.0,10.0,10.0,9.0,9.0,6,0.16 +57904,90.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.16 +2355,,,,,,,,0, +58877,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.11 +50368,,,,,,,,0, +72256,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.07 +38692,,,,,,,,0, +18323,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.2 +33616,,,,,,,,0, +10606,100.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.08 +23455,,,,,,,,0, +15685,,,,,,,,0, +57814,73.0,9.0,7.0,9.0,9.0,6.0,7.0,3,0.32 +74079,97.0,10.0,10.0,10.0,10.0,10.0,10.0,21,0.56 +75760,,,,,,,,0, +49885,81.0,9.0,8.0,9.0,9.0,10.0,8.0,93,2.54 +28291,98.0,10.0,9.0,10.0,10.0,9.0,10.0,58,1.59 +35120,73.0,8.0,9.0,9.0,9.0,10.0,7.0,4,0.12 +24133,,,,,,,,0, +55350,97.0,9.0,10.0,10.0,10.0,9.0,10.0,47,1.26 +9327,,,,,,,,0, +63665,97.0,10.0,9.0,10.0,10.0,10.0,10.0,30,0.83 +15223,99.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.87 +67151,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.1 +36908,92.0,10.0,10.0,9.0,10.0,10.0,9.0,16,0.43 +15095,92.0,9.0,9.0,10.0,9.0,8.0,9.0,17,0.47 +50028,96.0,10.0,10.0,10.0,10.0,9.0,10.0,73,1.96 +23415,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +65814,,,,,,,,0, +75569,,,,,,,,0, +23542,95.0,10.0,9.0,10.0,10.0,9.0,10.0,68,1.97 +55908,93.0,10.0,9.0,10.0,10.0,10.0,9.0,14,0.62 +7919,100.0,,10.0,,10.0,,,1,0.03 +57176,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.11 +72100,97.0,10.0,10.0,10.0,10.0,9.0,10.0,28,0.76 +49759,,,,,,,,0, +12801,,,,,,,,0, +64969,89.0,9.0,9.0,10.0,10.0,10.0,9.0,17,0.5 +46967,,,,,,,,0, +36797,97.0,10.0,10.0,10.0,10.0,9.0,10.0,93,2.56 +20001,98.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.36 +205,80.0,8.0,8.0,9.0,9.0,9.0,8.0,28,0.77 +47736,,,,,,,,0, +50587,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.21 +42022,,,,,,,,0, +57878,84.0,9.0,8.0,10.0,9.0,10.0,9.0,11,0.33 +22004,86.0,9.0,9.0,9.0,9.0,10.0,9.0,35,1.12 +75040,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.14 +17359,,,,,,,,0, +11362,40.0,8.0,2.0,8.0,10.0,10.0,4.0,2,0.06 +44772,60.0,6.0,8.0,10.0,10.0,6.0,8.0,1,0.04 +7363,,,,,,,,0, +64711,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.36 +1077,98.0,10.0,10.0,10.0,10.0,9.0,9.0,27,0.8 +56622,95.0,10.0,9.0,10.0,9.0,10.0,10.0,9,0.26 +17354,97.0,10.0,10.0,9.0,10.0,10.0,10.0,49,1.32 +30107,63.0,7.0,7.0,8.0,7.0,9.0,7.0,9,0.25 +56519,100.0,9.0,9.0,10.0,10.0,10.0,10.0,2,0.06 +57924,,,,,,,,0, +11318,100.0,9.0,9.0,10.0,10.0,9.0,10.0,4,0.29 +18730,84.0,9.0,8.0,9.0,10.0,9.0,8.0,73,2.01 +30076,,,,,,,,0, +21260,,,,,,,,0, +72804,96.0,10.0,10.0,10.0,10.0,10.0,10.0,29,0.8 +3197,81.0,8.0,8.0,9.0,9.0,9.0,8.0,113,3.03 +53941,78.0,8.0,8.0,9.0,9.0,9.0,8.0,136,3.66 +62751,,,,,,,,0, +6738,,,,,,,,0, +67578,90.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.06 +53309,90.0,10.0,6.0,10.0,10.0,10.0,9.0,2,0.06 +75347,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.53 +4196,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +50816,93.0,9.0,9.0,9.0,10.0,9.0,9.0,6,0.17 +59461,93.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.08 +2541,,,,,,,,0, +57597,100.0,9.0,9.0,10.0,10.0,10.0,9.0,2,0.05 +60769,94.0,10.0,9.0,10.0,10.0,10.0,10.0,31,0.83 +54120,90.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.06 +19796,98.0,10.0,10.0,10.0,10.0,10.0,10.0,24,0.67 +25639,96.0,10.0,10.0,9.0,10.0,10.0,10.0,40,1.18 +27917,99.0,10.0,10.0,9.0,10.0,10.0,10.0,14,0.51 +56522,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +11642,93.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.12 +1037,97.0,10.0,9.0,10.0,10.0,10.0,10.0,15,0.92 +3000,70.0,8.0,6.0,9.0,10.0,9.0,7.0,2,0.07 +9510,,,,,,,,0, +63246,100.0,10.0,10.0,10.0,10.0,10.0,10.0,51,1.42 +51096,81.0,9.0,8.0,9.0,9.0,9.0,9.0,43,9.42 +49317,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.27 +31342,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +17437,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.29 +57370,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +21993,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.14 +30291,,,,,,,,0, +60727,100.0,10.0,10.0,9.0,9.0,10.0,10.0,3,0.08 +26024,98.0,10.0,10.0,10.0,10.0,9.0,10.0,10,0.34 +27311,96.0,10.0,10.0,10.0,10.0,9.0,10.0,26,0.85 +29289,,,,,,,,0, +16226,,,,,,,,0, +10351,96.0,10.0,10.0,10.0,10.0,9.0,10.0,138,4.63 +8644,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.12 +55812,,,,,,,,0, +69083,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +199,98.0,10.0,10.0,10.0,10.0,10.0,10.0,61,1.68 +54206,98.0,10.0,10.0,10.0,10.0,10.0,9.0,11,0.58 +71379,,,,,,,,0, +43546,92.0,10.0,10.0,9.0,9.0,9.0,9.0,62,1.74 +6819,,,,,,,,0, +34354,95.0,10.0,9.0,10.0,10.0,10.0,9.0,30,1.08 +32830,,,,,,,,0, +51992,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,0.76 +64187,,,,,,,,0, +16408,99.0,10.0,10.0,10.0,10.0,10.0,10.0,24,0.67 +14403,97.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.17 +62100,98.0,10.0,10.0,10.0,10.0,10.0,9.0,33,0.9 +66112,,,,,,,,0, +21328,,,,,,,,0, +7982,96.0,10.0,10.0,10.0,10.0,9.0,10.0,15,0.42 +54151,97.0,10.0,9.0,10.0,10.0,9.0,9.0,18,0.51 +50280,98.0,10.0,10.0,10.0,10.0,10.0,9.0,43,1.18 +31616,99.0,10.0,9.0,10.0,10.0,10.0,10.0,53,1.48 +39030,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +38736,93.0,9.0,9.0,10.0,10.0,10.0,8.0,6,0.2 +62857,,,,,,,,1,0.07 +26399,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +10347,100.0,10.0,10.0,10.0,10.0,8.0,10.0,3,0.08 +33069,100.0,10.0,10.0,10.0,10.0,4.0,10.0,1,0.06 +72463,98.0,10.0,10.0,10.0,10.0,10.0,10.0,41,1.15 +59242,60.0,6.0,4.0,8.0,10.0,8.0,2.0,1,0.03 +29523,92.0,10.0,9.0,9.0,10.0,9.0,9.0,18,1.29 +5914,100.0,10.0,10.0,8.0,8.0,4.0,6.0,1,0.03 +56881,,,,,,,,0, +23619,100.0,10.0,10.0,10.0,10.0,8.0,10.0,5,0.14 +8371,94.0,10.0,10.0,10.0,10.0,9.0,9.0,44,1.22 +29398,,,,,,,,0, +72658,,,,,,,,0, +69982,80.0,8.0,8.0,8.0,8.0,8.0,8.0,1,0.03 +69723,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.06 +39198,,,,,,,,0, +46537,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.31 +47879,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.11 +74126,89.0,9.0,8.0,9.0,10.0,9.0,9.0,25,0.68 +43424,89.0,9.0,9.0,9.0,9.0,10.0,8.0,35,0.95 +19515,98.0,10.0,9.0,10.0,10.0,10.0,10.0,51,1.61 +55143,,,,,,,,0, +28533,,,,,,,,0, +63391,95.0,10.0,10.0,10.0,10.0,10.0,9.0,16,0.45 +52042,80.0,9.0,6.0,9.0,8.0,9.0,8.0,2,0.06 +38400,89.0,9.0,9.0,10.0,10.0,10.0,9.0,14,0.38 +72510,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.19 +40105,94.0,10.0,9.0,10.0,10.0,10.0,9.0,102,2.86 +27172,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +25945,98.0,10.0,10.0,10.0,10.0,9.0,10.0,104,3.23 +63842,,,,,,,,1,0.03 +45076,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +23539,88.0,9.0,8.0,9.0,10.0,9.0,9.0,23,0.75 +45272,85.0,9.0,8.0,8.0,9.0,10.0,9.0,10,0.28 +41070,,,,,,,,0, +10063,,,,,,,,0, +60295,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.05 +55308,96.0,10.0,9.0,10.0,10.0,8.0,10.0,5,0.14 +34058,,,,,,,,0, +25815,100.0,9.0,9.0,10.0,10.0,8.0,10.0,3,0.08 +60459,,,,,,,,0, +42978,95.0,10.0,10.0,10.0,10.0,9.0,9.0,30,0.93 +4632,92.0,10.0,9.0,10.0,10.0,10.0,9.0,17,0.48 +28215,,,,,,,,0, +51849,80.0,8.0,8.0,9.0,10.0,10.0,9.0,4,0.11 +19852,,,,,,,,0, +30274,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +22330,93.0,10.0,9.0,10.0,9.0,10.0,9.0,38,3.62 +20824,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +57223,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.66 +12286,95.0,10.0,10.0,9.0,10.0,10.0,10.0,21,0.76 +20784,92.0,9.0,9.0,10.0,9.0,10.0,9.0,39,1.11 +21210,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.1 +75642,,,,,,,,0, +6578,97.0,10.0,9.0,10.0,10.0,10.0,9.0,19,0.54 +37196,,,,,,,,0, +46117,85.0,10.0,9.0,9.0,10.0,9.0,9.0,4,0.11 +15893,,,,,,,,0, +11059,95.0,10.0,10.0,10.0,10.0,9.0,10.0,31,0.86 +27677,98.0,10.0,9.0,10.0,10.0,9.0,10.0,36,0.99 +58637,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +39358,93.0,9.0,10.0,10.0,10.0,9.0,9.0,3,0.08 +75534,98.0,10.0,9.0,10.0,10.0,10.0,10.0,9,0.26 +55599,90.0,9.0,9.0,9.0,9.0,9.0,9.0,12,0.33 +51144,88.0,9.0,9.0,10.0,9.0,9.0,9.0,19,0.56 +68919,85.0,9.0,9.0,9.0,10.0,9.0,9.0,8,0.22 +42274,96.0,10.0,9.0,10.0,10.0,10.0,9.0,51,1.78 +75237,93.0,10.0,10.0,10.0,10.0,10.0,9.0,16,0.46 +47710,98.0,10.0,10.0,10.0,10.0,9.0,10.0,8,0.26 +21730,89.0,9.0,9.0,9.0,9.0,9.0,9.0,45,1.24 +30029,98.0,10.0,10.0,10.0,10.0,10.0,10.0,42,1.3 +48419,98.0,10.0,10.0,10.0,10.0,10.0,10.0,77,2.22 +21971,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.14 +75595,,,,,,,,0, +4156,,,,,,,,0, +35796,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +26730,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.14 +65166,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +76548,,,,,,,,0, +8260,80.0,10.0,6.0,10.0,10.0,6.0,10.0,1,0.03 +70875,,,,,,,,0, +34920,,,,,,,,1,0.03 +53077,,,,,,,,0, +45411,93.0,9.0,8.0,10.0,10.0,10.0,9.0,3,0.08 +69355,99.0,10.0,10.0,10.0,10.0,9.0,10.0,28,0.78 +64872,,,,,,,,0, +44704,,,,,,,,0, +27572,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.17 +31211,,,,,,,,0, +36870,89.0,9.0,9.0,10.0,10.0,10.0,9.0,13,0.54 +20546,80.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.07 +1273,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +53259,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +42602,97.0,10.0,9.0,10.0,10.0,10.0,10.0,46,1.34 +1033,,,,,,,,0, +8589,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +66224,80.0,10.0,8.0,10.0,10.0,6.0,8.0,1,0.03 +13149,93.0,10.0,8.0,10.0,10.0,9.0,9.0,23,0.64 +69198,100.0,8.0,10.0,10.0,10.0,8.0,10.0,1,0.03 +55205,95.0,10.0,10.0,10.0,10.0,9.0,10.0,42,1.21 +23902,97.0,10.0,10.0,10.0,10.0,10.0,10.0,40,1.15 +15126,,,,,,,,0, +325,80.0,10.0,6.0,10.0,10.0,10.0,8.0,1,0.03 +65365,97.0,10.0,10.0,10.0,10.0,9.0,9.0,14,0.39 +42096,91.0,9.0,9.0,9.0,9.0,10.0,9.0,75,2.06 +26311,96.0,10.0,9.0,10.0,10.0,10.0,9.0,18,0.5 +51899,93.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.2 +58078,40.0,8.0,4.0,6.0,4.0,2.0,4.0,1,0.03 +12814,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +25166,96.0,10.0,9.0,10.0,10.0,9.0,10.0,17,0.48 +41393,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.03 +23973,97.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.17 +5682,94.0,10.0,10.0,10.0,10.0,9.0,9.0,57,1.59 +16380,,,,,,,,0, +61350,95.0,10.0,10.0,9.0,9.0,10.0,9.0,52,1.46 +23792,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +71700,90.0,9.0,9.0,9.0,9.0,8.0,9.0,2,0.08 +76891,80.0,10.0,6.0,4.0,8.0,10.0,6.0,1,0.03 +10115,82.0,9.0,8.0,9.0,10.0,8.0,8.0,12,0.33 +61391,,,,,,,,0, +29116,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +31870,,,,,,,,0, +37708,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.03 +23540,,,,,,,,0, +60615,97.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.2 +55888,87.0,9.0,9.0,8.0,9.0,9.0,9.0,12,0.34 +58898,95.0,10.0,10.0,8.0,9.0,9.0,9.0,11,0.35 +55909,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.53 +14652,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +18561,96.0,9.0,10.0,10.0,10.0,9.0,10.0,52,1.45 +51126,,,,,,,,0, +7529,,,,,,,,0, +1335,86.0,9.0,8.0,9.0,10.0,10.0,9.0,45,1.23 +599,,,,,,,,0, +65318,89.0,9.0,9.0,9.0,9.0,9.0,9.0,39,1.08 +56302,85.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.17 +72677,98.0,10.0,10.0,10.0,10.0,10.0,10.0,41,1.16 +46254,98.0,10.0,9.0,10.0,10.0,10.0,10.0,32,0.88 +66902,,,,,,,,0, +71087,,,,,,,,0, +30340,,,,,,,,0, +70912,92.0,10.0,9.0,10.0,10.0,9.0,9.0,33,0.96 +43132,95.0,10.0,10.0,10.0,9.0,9.0,9.0,12,0.34 +49435,80.0,9.0,9.0,8.0,10.0,9.0,9.0,2,0.06 +44674,,,,,,,,0, +42557,96.0,9.0,9.0,10.0,10.0,10.0,10.0,5,0.14 +34512,98.0,10.0,10.0,10.0,10.0,10.0,10.0,23,0.84 +4745,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.08 +50807,,,,,,,,0, +74814,96.0,10.0,10.0,9.0,9.0,10.0,9.0,93,2.62 +73036,80.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.03 +9609,,,,,,,,0, +69288,99.0,10.0,10.0,10.0,10.0,10.0,10.0,55,1.52 +40635,94.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.36 +48314,,,,,,,,0, +22142,,,,,,,,0, +32940,60.0,7.0,7.0,7.0,7.0,9.0,6.0,3,0.1 +76266,,,,,,,,0, +24001,92.0,10.0,9.0,10.0,10.0,9.0,9.0,10,0.35 +44794,,,,,,,,0, +36128,93.0,10.0,9.0,10.0,10.0,9.0,10.0,9,0.25 +27683,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.23 +27116,,,,,,,,0, +25595,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.37 +46360,99.0,10.0,9.0,9.0,10.0,10.0,10.0,17,0.46 +47849,80.0,8.0,8.0,10.0,8.0,8.0,8.0,1,0.03 +65619,,,,,,,,0, +69604,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,3.16 +69102,85.0,9.0,8.0,9.0,9.0,9.0,9.0,16,0.46 +64394,95.0,10.0,10.0,9.0,10.0,10.0,9.0,116,3.22 +59488,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.28 +29518,97.0,10.0,10.0,10.0,10.0,10.0,10.0,62,1.76 +6868,,,,,,,,0, +8046,84.0,8.0,8.0,9.0,9.0,9.0,9.0,19,0.77 +13172,,,,,,,,0, +67169,80.0,8.0,6.0,8.0,10.0,10.0,,1,0.04 +1517,,,,,,,,0, +45099,98.0,10.0,10.0,10.0,10.0,10.0,10.0,42,1.2 +57652,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.11 +34784,93.0,10.0,9.0,10.0,10.0,10.0,9.0,51,1.42 +20983,100.0,9.0,10.0,10.0,10.0,10.0,9.0,5,0.16 +65433,,,,,,,,0, +32858,99.0,10.0,10.0,10.0,10.0,10.0,10.0,150,4.25 +17005,100.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.52 +63107,100.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.56 +53699,91.0,10.0,9.0,10.0,10.0,9.0,9.0,66,1.93 +41476,96.0,10.0,10.0,10.0,10.0,10.0,10.0,38,1.43 +61776,,,,,,,,0, +33198,84.0,9.0,9.0,10.0,10.0,10.0,9.0,24,0.67 +54106,98.0,10.0,10.0,10.0,10.0,10.0,10.0,28,0.78 +27470,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +52901,80.0,8.0,8.0,9.0,9.0,9.0,8.0,6,0.21 +13672,,,,,,,,0, +48946,99.0,10.0,10.0,10.0,10.0,10.0,10.0,55,2.08 +23749,,,,,,,,0, +19300,,,,,,,,0, +23985,,,,,,,,0, +5625,87.0,9.0,10.0,9.0,9.0,8.0,9.0,148,4.1 +47197,,,,,,,,0, +48390,92.0,9.0,9.0,10.0,10.0,8.0,9.0,22,0.66 +48848,100.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.03 +15996,77.0,9.0,8.0,9.0,8.0,8.0,8.0,12,0.35 +76935,,,,,,,,0, +5290,,,,,,,,0, +59810,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,1.07 +44286,92.0,10.0,10.0,9.0,9.0,9.0,10.0,15,0.42 +30021,89.0,9.0,10.0,9.0,10.0,8.0,9.0,9,0.27 +23457,70.0,7.0,7.0,7.0,9.0,8.0,7.0,4,0.45 +66582,100.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.06 +40863,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +27189,88.0,9.0,9.0,10.0,10.0,9.0,9.0,36,1.02 +53999,86.0,9.0,8.0,9.0,9.0,9.0,9.0,45,1.3 +13441,,,,,,,,0, +24265,,,,,,,,0, +15197,90.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.17 +55217,98.0,10.0,9.0,10.0,10.0,10.0,9.0,13,0.67 +59579,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +55010,96.0,10.0,10.0,10.0,10.0,10.0,10.0,68,1.99 +19176,94.0,9.0,9.0,10.0,10.0,10.0,9.0,10,0.31 +6902,,,,,,,,0, +22327,,,,,,,,0, +32365,96.0,10.0,10.0,10.0,10.0,10.0,9.0,49,1.44 +43270,,,,,,,,0, +1094,,,,,,,,0, +39932,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.41 +58836,,,,,,,,0, +45105,99.0,10.0,10.0,10.0,10.0,10.0,10.0,55,1.54 +68513,,,,,,,,0, +48460,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.23 +52388,91.0,9.0,10.0,10.0,10.0,9.0,9.0,7,0.2 +66964,,,,,,,,0, +53760,,,,,,,,0, +66094,77.0,8.0,8.0,9.0,9.0,9.0,8.0,104,2.88 +19706,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.11 +25136,93.0,10.0,9.0,9.0,9.0,9.0,9.0,9,0.54 +35876,80.0,10.0,10.0,8.0,8.0,10.0,8.0,1,0.03 +9343,100.0,8.0,10.0,10.0,8.0,8.0,6.0,1,0.03 +6395,,,,,,,,0, +30391,,,,,,,,0, +25797,,,,,,,,0, +13751,96.0,10.0,10.0,10.0,10.0,10.0,10.0,50,1.41 +25313,,,,,,,,0, +40201,,,,,,,,0, +73358,95.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.99 +21752,97.0,10.0,10.0,10.0,10.0,10.0,9.0,28,0.79 +69002,94.0,9.0,9.0,9.0,10.0,10.0,9.0,23,0.64 +17701,95.0,10.0,9.0,10.0,10.0,10.0,10.0,15,0.54 +25803,98.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.74 +53483,93.0,10.0,9.0,10.0,10.0,9.0,9.0,13,0.66 +65642,,,,,,,,0, +2779,,,,,,,,1,0.03 +3005,92.0,10.0,10.0,10.0,10.0,9.0,9.0,45,1.28 +37833,93.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.17 +8451,95.0,10.0,10.0,9.0,10.0,9.0,9.0,60,1.87 +44604,100.0,10.0,10.0,10.0,8.0,8.0,8.0,1,0.03 +1547,96.0,10.0,10.0,10.0,10.0,9.0,10.0,38,1.08 +55758,92.0,10.0,9.0,9.0,10.0,10.0,9.0,12,0.37 +61235,60.0,10.0,2.0,2.0,8.0,6.0,4.0,1,0.03 +487,,,,,,,,0, +34657,100.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.21 +73444,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.1 +52147,90.0,9.0,9.0,9.0,9.0,10.0,9.0,49,1.52 +11678,100.0,10.0,10.0,10.0,10.0,10.0,8.0,2,0.06 +68583,98.0,10.0,10.0,10.0,10.0,10.0,10.0,27,2.89 +15348,,,,,,,,0, +52022,94.0,10.0,9.0,10.0,10.0,10.0,10.0,26,0.9 +35879,,,,,,,,0, +4709,78.0,8.0,8.0,9.0,9.0,9.0,8.0,117,3.42 +29917,91.0,10.0,9.0,10.0,10.0,9.0,10.0,7,0.35 +23296,,,,,,,,0, +32905,99.0,10.0,10.0,10.0,10.0,10.0,10.0,32,1.56 +28630,,,,,,,,0, +57907,96.0,9.0,9.0,9.0,10.0,9.0,10.0,9,0.26 +59601,93.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.09 +61370,,,,,,,,0, +44048,94.0,9.0,9.0,10.0,10.0,9.0,9.0,46,1.37 +13524,,,,,,,,0, +30931,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.04 +32142,,,,,,,,0, +57936,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +73357,99.0,10.0,10.0,10.0,10.0,10.0,10.0,74,2.3 +48847,80.0,10.0,4.0,10.0,10.0,8.0,8.0,1,0.55 +22480,67.0,7.0,6.0,9.0,8.0,9.0,7.0,16,0.65 +46248,,,,,,,,1,0.04 +41373,74.0,8.0,8.0,9.0,10.0,9.0,7.0,11,0.37 +51308,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.36 +64263,100.0,10.0,9.0,9.0,10.0,9.0,9.0,3,0.31 +50939,100.0,10.0,10.0,10.0,10.0,9.0,10.0,39,1.18 +33432,50.0,6.0,6.0,8.0,5.0,7.0,5.0,5,0.32 +3461,96.0,10.0,10.0,10.0,10.0,9.0,10.0,31,0.9 +52243,,,,,,,,0, +62497,,,,,,,,0, +21777,97.0,10.0,9.0,10.0,10.0,10.0,10.0,20,0.57 +16607,80.0,8.0,8.0,9.0,8.0,10.0,7.0,7,0.27 +32903,,,,,,,,0, +24775,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,1.25 +26677,93.0,9.0,9.0,9.0,9.0,10.0,9.0,15,0.44 +6284,96.0,10.0,10.0,10.0,10.0,10.0,10.0,126,3.61 +38945,,,,,,,,0, +57575,,,,,,,,0, +36593,,,,,,,,0, +45265,,,,,,,,0, +71631,93.0,10.0,9.0,9.0,10.0,9.0,10.0,3,0.09 +53102,,,,,,,,0, +69462,93.0,9.0,9.0,9.0,9.0,10.0,9.0,20,0.63 +41849,76.0,8.0,8.0,10.0,9.0,9.0,8.0,27,0.86 +55481,94.0,10.0,10.0,10.0,10.0,10.0,9.0,48,1.37 +46740,94.0,10.0,10.0,10.0,10.0,10.0,9.0,69,1.97 +8750,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,0.03 +68993,93.0,10.0,10.0,10.0,10.0,8.0,10.0,6,0.2 +74960,96.0,10.0,10.0,10.0,10.0,9.0,10.0,40,1.15 +46820,88.0,8.0,9.0,10.0,10.0,10.0,9.0,272,8.32 +65293,,,,,,,,0, +12356,95.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.69 +19723,94.0,10.0,10.0,10.0,10.0,10.0,10.0,146,4.26 +10409,96.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.45 +62424,,,,,,,,0, +51412,98.0,10.0,10.0,10.0,10.0,10.0,10.0,32,0.92 +28169,97.0,10.0,10.0,10.0,10.0,9.0,10.0,81,2.45 +38821,94.0,10.0,9.0,10.0,9.0,9.0,9.0,63,1.84 +70207,98.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.51 +44690,,,,,,,,0, +7440,98.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.32 +68074,,,,,,,,0, +26651,80.0,10.0,6.0,10.0,10.0,10.0,8.0,1,0.03 +58511,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.23 +20043,60.0,10.0,10.0,4.0,4.0,8.0,8.0,1,0.48 +75068,97.0,10.0,9.0,10.0,10.0,10.0,9.0,36,1.12 +41356,96.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.41 +56985,,,,,,,,0, +43159,99.0,10.0,10.0,10.0,10.0,10.0,10.0,34,1.29 +52461,95.0,9.0,9.0,10.0,10.0,10.0,9.0,20,0.59 +23938,94.0,10.0,10.0,10.0,10.0,9.0,10.0,196,5.75 +12985,98.0,10.0,10.0,10.0,10.0,9.0,9.0,64,5.0 +49742,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.12 +42539,,,,,,,,0, +20813,,,,,,,,0, +26515,91.0,10.0,9.0,10.0,10.0,10.0,9.0,89,2.53 +62570,92.0,9.0,9.0,9.0,10.0,9.0,9.0,101,2.95 +51421,95.0,10.0,9.0,10.0,10.0,10.0,10.0,40,1.15 +73242,93.0,9.0,10.0,9.0,9.0,9.0,9.0,32,1.0 +58542,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.61 +62878,,,,,,,,0, +24454,,,,,,,,0, +51628,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +32418,,,,,,,,0, +56429,94.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.38 +66548,93.0,10.0,9.0,10.0,10.0,10.0,9.0,69,1.97 +14065,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.15 +68792,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.14 +6107,95.0,10.0,9.0,10.0,10.0,10.0,10.0,28,1.23 +34714,,,,,,,,0, +73573,,,,,,,,0, +37848,,,,,,,,0, +58298,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.51 +42799,98.0,10.0,10.0,10.0,10.0,10.0,10.0,29,0.84 +34122,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.16 +12499,,,,,,,,0, +2674,60.0,6.0,8.0,10.0,10.0,10.0,6.0,1,0.03 +20042,,,,,,,,0, +69445,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.14 +26565,96.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.18 +59389,92.0,9.0,9.0,10.0,10.0,9.0,9.0,70,2.61 +72749,,,,,,,,0, +66109,93.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.28 +24011,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.37 +7148,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.93 +23955,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.39 +3905,100.0,10.0,10.0,9.0,10.0,10.0,10.0,18,0.54 +20897,95.0,10.0,10.0,10.0,10.0,10.0,10.0,22,0.64 +18430,,,,,,,,0, +17929,,,,,,,,0, +43881,88.0,9.0,9.0,9.0,9.0,9.0,9.0,8,1.4 +46991,,,,,,,,0, +19831,94.0,10.0,10.0,10.0,10.0,9.0,10.0,67,1.94 +33022,98.0,10.0,9.0,10.0,10.0,9.0,10.0,45,1.29 +56081,,,,,,,,0, +72361,,,,,,,,0, +30976,97.0,10.0,9.0,10.0,10.0,10.0,10.0,21,2.73 +15919,100.0,10.0,10.0,10.0,10.0,10.0,10.0,27,0.83 +22191,90.0,9.0,9.0,10.0,10.0,10.0,9.0,26,0.78 +35726,99.0,10.0,10.0,10.0,10.0,10.0,10.0,49,1.46 +70493,,,,,,,,0, +60479,96.0,10.0,10.0,9.0,10.0,10.0,10.0,5,0.15 +12519,,,,,,,,0, +24256,,,,,,,,0, +59126,90.0,9.0,9.0,9.0,9.0,9.0,10.0,23,0.71 +16121,93.0,10.0,10.0,9.0,9.0,10.0,10.0,4,0.13 +14206,95.0,10.0,10.0,10.0,10.0,9.0,10.0,22,0.66 +64268,93.0,9.0,9.0,10.0,10.0,8.0,8.0,6,0.18 +71942,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +3504,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.21 +53670,82.0,9.0,8.0,9.0,9.0,9.0,9.0,9,0.27 +63344,100.0,10.0,9.0,10.0,9.0,10.0,10.0,2,0.07 +59122,93.0,9.0,10.0,10.0,10.0,10.0,9.0,3,0.09 +61413,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.36 +7996,98.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.9 +69677,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +58240,97.0,10.0,9.0,10.0,10.0,10.0,10.0,46,1.41 +76203,,,,,,,,0, +72791,88.0,9.0,8.0,10.0,10.0,10.0,9.0,22,0.65 +42060,97.0,10.0,9.0,10.0,10.0,10.0,10.0,26,0.78 +60538,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +21163,97.0,10.0,10.0,10.0,10.0,9.0,10.0,20,0.69 +21010,95.0,10.0,10.0,10.0,10.0,9.0,10.0,22,0.67 +30152,88.0,9.0,7.0,10.0,10.0,10.0,10.0,8,0.23 +76446,,,,,,,,0, +21385,94.0,10.0,9.0,10.0,10.0,8.0,9.0,13,0.38 +10493,85.0,9.0,9.0,9.0,9.0,9.0,9.0,12,0.35 +2622,95.0,10.0,10.0,10.0,10.0,10.0,10.0,56,1.76 +3077,,,,,,,,0, +52612,,,,,,,,0, +10441,,,,,,,,0, +51801,,,,,,,,0, +45242,90.0,9.0,8.0,9.0,9.0,9.0,10.0,2,0.08 +55560,,,,,,,,0, +60893,98.0,10.0,9.0,10.0,10.0,10.0,10.0,9,0.29 +12200,99.0,10.0,10.0,10.0,10.0,10.0,10.0,57,1.72 +46164,90.0,9.0,8.0,9.0,10.0,9.0,9.0,14,0.45 +13629,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.37 +74761,,,,,,,,0, +14737,100.0,9.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +63938,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,0.89 +8505,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +28690,93.0,9.0,9.0,10.0,10.0,10.0,9.0,6,0.68 +58442,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.09 +73182,98.0,10.0,10.0,10.0,10.0,9.0,10.0,33,1.01 +67577,95.0,10.0,10.0,9.0,9.0,8.0,10.0,8,0.26 +11332,73.0,8.0,7.0,9.0,8.0,10.0,8.0,14,0.41 +49058,96.0,10.0,10.0,10.0,10.0,9.0,10.0,50,1.48 +17688,97.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.44 +34533,96.0,10.0,10.0,10.0,10.0,10.0,10.0,177,5.33 +486,94.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.21 +57531,80.0,10.0,10.0,10.0,9.0,9.0,10.0,2,0.06 +10386,70.0,8.0,7.0,8.0,7.0,8.0,2.0,2,0.07 +53604,96.0,10.0,10.0,10.0,10.0,10.0,10.0,95,2.77 +48670,98.0,10.0,10.0,10.0,10.0,10.0,10.0,71,2.07 +57192,70.0,9.0,9.0,9.0,9.0,9.0,8.0,3,0.58 +70286,89.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.27 +74741,99.0,10.0,10.0,10.0,10.0,10.0,10.0,64,2.05 +26014,,,,,,,,0, +59197,97.0,10.0,9.0,10.0,10.0,10.0,10.0,15,1.94 +56916,,,,,,,,0, +60239,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.58 +30779,,,,,,,,0, +64334,100.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.29 +37605,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.08 +63371,98.0,10.0,10.0,10.0,10.0,10.0,10.0,37,1.15 +69080,88.0,9.0,10.0,9.0,9.0,9.0,9.0,10,0.29 +10854,,,,,,,,0, +20356,,,,,,,,0, +35448,93.0,10.0,9.0,10.0,9.0,10.0,9.0,38,1.12 +2333,96.0,10.0,10.0,10.0,10.0,10.0,9.0,113,3.36 +72808,,,,,,,,0, +76428,90.0,9.0,9.0,10.0,10.0,9.0,9.0,55,1.64 +56611,99.0,10.0,10.0,10.0,10.0,10.0,10.0,38,1.12 +68194,93.0,9.0,9.0,10.0,10.0,10.0,9.0,6,0.2 +10836,89.0,10.0,9.0,10.0,9.0,10.0,9.0,15,0.57 +38905,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +59129,98.0,10.0,10.0,10.0,10.0,10.0,10.0,47,1.53 +42432,97.0,10.0,10.0,9.0,9.0,10.0,10.0,8,0.28 +40350,,,,,,,,0, +59604,,,,,,,,0, +66985,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.29 +20538,,,,,,,,0, +25549,96.0,10.0,10.0,10.0,10.0,9.0,10.0,47,1.39 +33305,94.0,10.0,9.0,10.0,10.0,10.0,10.0,33,1.04 +70833,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.24 +28908,,,,,,,,0, +70502,98.0,10.0,10.0,10.0,10.0,9.0,10.0,23,0.8 +73013,95.0,10.0,9.0,10.0,10.0,10.0,9.0,37,1.17 +33749,99.0,10.0,10.0,10.0,10.0,10.0,10.0,27,0.82 +67520,94.0,9.0,10.0,10.0,10.0,10.0,9.0,53,1.61 +75360,,,,,,,,0, +15053,90.0,10.0,9.0,10.0,8.0,10.0,10.0,3,0.09 +54848,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +11467,60.0,6.0,2.0,8.0,6.0,10.0,6.0,2,0.06 +47123,92.0,10.0,8.0,9.0,9.0,9.0,8.0,7,0.23 +37567,,,,,,,,2,0.12 +47572,93.0,10.0,9.0,9.0,9.0,10.0,9.0,6,0.2 +31622,,,,,,,,0, +65150,91.0,9.0,9.0,10.0,10.0,10.0,9.0,10,0.34 +68031,,,,,,,,0, +76441,99.0,10.0,10.0,10.0,10.0,10.0,10.0,100,2.97 +59312,80.0,10.0,10.0,10.0,10.0,6.0,10.0,1,0.03 +73293,94.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.44 +20608,,,,,,,,0, +64161,99.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.71 +50189,60.0,7.0,7.0,6.0,6.0,8.0,8.0,5,0.16 +49627,80.0,6.0,8.0,9.0,9.0,8.0,7.0,3,0.86 +45195,99.0,10.0,10.0,10.0,10.0,10.0,10.0,56,1.71 +25010,93.0,10.0,10.0,7.0,9.0,10.0,9.0,3,0.1 +51074,95.0,9.0,9.0,10.0,10.0,10.0,9.0,13,0.38 +41540,,,,,,,,0, +68353,97.0,10.0,9.0,10.0,10.0,10.0,10.0,12,0.59 +34678,98.0,10.0,9.0,10.0,10.0,10.0,9.0,51,1.5 +30370,,,,,,,,0, +15131,,,,,,,,0, +75859,95.0,9.0,9.0,10.0,10.0,9.0,9.0,47,1.46 +43382,74.0,8.0,9.0,9.0,9.0,9.0,9.0,7,0.21 +14935,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.34 +43693,96.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.95 +55104,98.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.6 +76768,95.0,10.0,8.0,9.0,10.0,9.0,9.0,36,1.07 +71978,93.0,10.0,8.0,10.0,10.0,9.0,9.0,3,0.09 +15717,100.0,10.0,10.0,10.0,10.0,9.0,10.0,12,0.37 +37832,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,0.03 +39787,80.0,9.0,7.0,8.0,9.0,9.0,8.0,8,0.28 +76569,92.0,9.0,9.0,10.0,10.0,10.0,9.0,21,0.66 +5165,,,,,,,,0, +6650,97.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.5 +74746,96.0,10.0,10.0,9.0,10.0,9.0,10.0,11,0.33 +14296,93.0,10.0,10.0,10.0,10.0,10.0,9.0,11,2.16 +1564,,,,,,,,0, +5131,80.0,6.0,6.0,10.0,10.0,10.0,8.0,1,0.03 +2607,100.0,10.0,10.0,10.0,10.0,9.0,10.0,15,0.46 +56375,93.0,9.0,9.0,9.0,10.0,9.0,9.0,60,1.89 +29296,99.0,10.0,10.0,10.0,10.0,9.0,10.0,18,0.78 +20668,,,,,,,,0, +65438,,,,,,,,0, +25796,,,,,,,,0, +71102,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,0.09 +42644,,,,,,,,0, +1341,81.0,8.0,7.0,9.0,9.0,8.0,8.0,62,1.88 +11195,90.0,9.0,10.0,10.0,9.0,10.0,10.0,4,0.13 +32470,,,,,,,,0, +31701,96.0,10.0,10.0,10.0,10.0,10.0,9.0,56,1.67 +66425,82.0,8.0,8.0,8.0,9.0,9.0,8.0,53,1.58 +24132,92.0,9.0,8.0,10.0,10.0,10.0,9.0,47,1.43 +51388,78.0,8.0,10.0,9.0,9.0,9.0,8.0,12,0.42 +28272,98.0,10.0,10.0,10.0,10.0,10.0,10.0,38,1.14 +72985,94.0,9.0,9.0,10.0,10.0,10.0,10.0,18,0.61 +48199,,,,,,,,0, +36929,93.0,10.0,10.0,10.0,10.0,10.0,9.0,102,3.08 +3758,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +57877,,,,,,,,0, +38535,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +56044,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.1 +75134,81.0,9.0,8.0,9.0,9.0,8.0,8.0,38,1.17 +27098,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.09 +56394,85.0,9.0,9.0,9.0,9.0,10.0,9.0,11,0.33 +7043,95.0,10.0,10.0,10.0,10.0,10.0,9.0,35,1.05 +22269,,,,,,,,0, +75856,,,,,,,,0, +3569,80.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.03 +8284,67.0,6.0,7.0,10.0,10.0,10.0,7.0,10,0.32 +51799,94.0,9.0,10.0,10.0,9.0,10.0,10.0,68,2.08 +15397,,,,,,,,0, +6612,99.0,10.0,9.0,10.0,10.0,10.0,10.0,15,0.62 +45762,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.18 +16250,83.0,9.0,9.0,9.0,9.0,9.0,9.0,6,0.31 +61622,,,,,,,,0, +32118,83.0,9.0,7.0,9.0,9.0,9.0,9.0,6,0.38 +52735,73.0,8.0,8.0,9.0,9.0,9.0,8.0,174,5.2 +26957,94.0,10.0,10.0,10.0,10.0,10.0,9.0,10,0.3 +16987,93.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.92 +75933,,,,,,,,0, +4978,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.18 +34813,,,,,,,,0, +31415,,,,,,,,0, +5002,85.0,8.0,9.0,8.0,9.0,9.0,8.0,25,0.77 +44600,96.0,10.0,10.0,9.0,10.0,10.0,10.0,16,0.93 +5375,85.0,9.0,8.0,9.0,8.0,9.0,9.0,14,0.42 +3278,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +15217,99.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.5 +15634,94.0,10.0,10.0,10.0,10.0,10.0,9.0,35,1.3 +57391,,,,,,,,0, +49347,90.0,10.0,9.0,9.0,9.0,9.0,9.0,21,0.66 +47406,95.0,10.0,10.0,9.0,9.0,9.0,9.0,8,0.24 +6592,83.0,9.0,9.0,10.0,10.0,8.0,9.0,8,0.27 +15726,90.0,9.0,8.0,9.0,10.0,9.0,9.0,3,0.09 +46261,89.0,10.0,10.0,10.0,10.0,10.0,9.0,58,2.19 +48374,94.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.54 +8203,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.36 +6181,90.0,9.0,10.0,10.0,10.0,8.0,10.0,4,0.13 +15214,,,,,,,,0, +54852,,,,,,,,0, +47668,,,,,,,,0, +7836,93.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.49 +22302,,,,,,,,1,0.08 +10758,100.0,10.0,10.0,10.0,10.0,9.0,10.0,48,1.42 +35866,98.0,10.0,9.0,10.0,10.0,10.0,10.0,13,0.39 +48380,78.0,8.0,8.0,9.0,9.0,10.0,8.0,146,4.44 +4893,91.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.29 +21759,98.0,10.0,9.0,10.0,10.0,9.0,10.0,44,1.35 +35639,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +12298,,,,,,,,1,0.08 +3239,92.0,10.0,9.0,10.0,10.0,9.0,9.0,20,0.6 +41493,90.0,9.0,8.0,10.0,10.0,9.0,9.0,44,1.6 +51215,99.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.96 +34476,98.0,10.0,10.0,10.0,10.0,10.0,10.0,24,0.73 +9056,80.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.03 +50853,80.0,8.0,8.0,9.0,9.0,10.0,8.0,111,3.31 +40459,98.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.89 +297,79.0,9.0,9.0,9.0,9.0,9.0,8.0,57,1.72 +1079,97.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.64 +32037,80.0,8.0,8.0,8.0,6.0,6.0,8.0,1,0.03 +17400,76.0,8.0,8.0,9.0,9.0,10.0,8.0,167,4.97 +7353,85.0,9.0,8.0,9.0,10.0,10.0,9.0,39,1.16 +58456,100.0,10.0,10.0,10.0,10.0,10.0,10.0,108,3.21 +560,,,,,,,,0, +3058,89.0,10.0,9.0,10.0,10.0,8.0,9.0,9,0.31 +39965,98.0,10.0,10.0,10.0,10.0,10.0,10.0,46,1.43 +44143,,,,,,,,0, +35894,93.0,10.0,10.0,10.0,10.0,8.0,9.0,3,0.1 +5728,,,,,,,,0, +36789,91.0,10.0,10.0,10.0,10.0,9.0,9.0,7,0.26 +72294,,,,,,,,0, +58723,,,,,,,,1,0.03 +9714,80.0,9.0,7.0,9.0,9.0,8.0,7.0,2,0.06 +72301,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.2 +24072,90.0,9.0,7.0,10.0,10.0,8.0,9.0,2,0.06 +36766,94.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.21 +1802,92.0,9.0,10.0,9.0,9.0,10.0,9.0,18,0.56 +7926,,,,,,,,0, +24822,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.3 +65581,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +66838,96.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.37 +31271,80.0,9.0,7.0,9.0,9.0,8.0,9.0,3,0.09 +34391,,,,,,,,0, +68245,100.0,10.0,7.0,9.0,10.0,9.0,10.0,4,0.12 +57450,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.66 +42542,95.0,10.0,10.0,10.0,10.0,9.0,9.0,48,1.48 +17693,93.0,9.0,10.0,10.0,10.0,9.0,10.0,7,0.33 +12844,,,,,,,,0, +10648,93.0,10.0,10.0,10.0,10.0,9.0,9.0,27,0.85 +56315,98.0,10.0,10.0,10.0,10.0,10.0,10.0,54,1.68 +41368,92.0,10.0,10.0,10.0,9.0,9.0,9.0,10,0.34 +32608,85.0,9.0,8.0,9.0,9.0,9.0,8.0,30,0.9 +41938,87.0,9.0,9.0,9.0,9.0,9.0,9.0,72,2.15 +17444,87.0,9.0,10.0,9.0,9.0,9.0,9.0,62,1.94 +18780,99.0,10.0,10.0,10.0,10.0,9.0,10.0,97,2.93 +34470,95.0,10.0,9.0,10.0,10.0,10.0,10.0,12,0.41 +6652,,,,,,,,0, +49879,91.0,10.0,9.0,9.0,10.0,9.0,9.0,68,2.06 +42753,80.0,9.0,8.0,8.0,9.0,9.0,9.0,18,0.54 +47911,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.18 +15256,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,4.13 +75525,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,1.46 +20766,94.0,10.0,10.0,10.0,10.0,9.0,9.0,66,1.99 +11647,100.0,9.0,9.0,9.0,10.0,9.0,10.0,4,0.12 +27074,87.0,9.0,9.0,9.0,10.0,9.0,9.0,67,2.08 +19091,96.0,10.0,9.0,10.0,10.0,10.0,10.0,121,4.02 +14999,87.0,9.0,9.0,10.0,10.0,10.0,9.0,20,0.6 +75155,93.0,10.0,9.0,9.0,10.0,9.0,9.0,39,6.43 +47403,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +2824,95.0,10.0,9.0,10.0,10.0,9.0,10.0,63,1.93 +50149,79.0,9.0,8.0,9.0,9.0,9.0,8.0,100,3.1 +5462,,,,,,,,0, +54346,98.0,10.0,10.0,9.0,10.0,10.0,10.0,71,2.2 +34128,99.0,10.0,10.0,10.0,10.0,10.0,10.0,33,1.19 +63720,84.0,9.0,8.0,10.0,9.0,10.0,9.0,174,5.36 +62526,78.0,9.0,8.0,9.0,9.0,10.0,8.0,183,5.52 +11105,84.0,9.0,9.0,9.0,9.0,10.0,9.0,151,4.51 +26309,,,,,,,,0, +19060,80.0,10.0,6.0,8.0,10.0,8.0,8.0,2,0.06 +9811,98.0,10.0,9.0,10.0,10.0,9.0,9.0,11,0.54 +29605,,,,,,,,0, +46486,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +42909,89.0,9.0,8.0,10.0,10.0,10.0,9.0,59,1.87 +55092,90.0,10.0,9.0,9.0,10.0,8.0,7.0,2,0.06 +22891,97.0,10.0,10.0,10.0,10.0,10.0,10.0,42,1.28 +23385,72.0,8.0,7.0,9.0,9.0,9.0,8.0,145,4.35 +3822,76.0,8.0,8.0,9.0,9.0,10.0,8.0,207,6.21 +76780,70.0,7.0,8.0,8.0,7.0,7.0,7.0,2,0.07 +11922,84.0,8.0,8.0,9.0,9.0,8.0,9.0,35,1.09 +34833,,,,,,,,0, +10902,83.0,9.0,9.0,9.0,8.0,8.0,9.0,28,0.87 +41992,90.0,9.0,9.0,9.0,9.0,8.0,9.0,4,0.15 +55656,,,,,,,,0, +50711,88.0,10.0,8.0,10.0,10.0,8.0,9.0,29,0.96 +48934,,,,,,,,0, +49142,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.1 +52047,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +21541,83.0,9.0,9.0,8.0,8.0,9.0,9.0,48,1.5 +57901,,,,,,,,0, +61462,80.0,10.0,6.0,8.0,6.0,10.0,6.0,1,0.03 +10148,72.0,8.0,7.0,9.0,9.0,10.0,8.0,205,6.19 +22596,96.0,9.0,9.0,10.0,10.0,10.0,10.0,32,1.0 +38689,94.0,10.0,9.0,10.0,10.0,9.0,10.0,33,1.28 +18701,78.0,9.0,8.0,9.0,9.0,10.0,8.0,159,4.81 +59082,95.0,10.0,9.0,10.0,10.0,10.0,10.0,67,2.02 +61261,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.66 +7179,98.0,10.0,10.0,10.0,10.0,9.0,10.0,47,2.88 +54370,96.0,10.0,10.0,10.0,10.0,9.0,9.0,10,0.34 +98,,,,,,,,0, +60716,77.0,8.0,8.0,9.0,8.0,8.0,8.0,23,0.71 +42492,82.0,9.0,8.0,9.0,9.0,9.0,9.0,43,1.33 +22378,83.0,9.0,8.0,9.0,9.0,9.0,9.0,55,1.72 +65982,85.0,9.0,9.0,9.0,9.0,8.0,9.0,62,1.89 +24142,86.0,9.0,9.0,9.0,8.0,9.0,9.0,64,1.99 +52337,82.0,9.0,9.0,9.0,9.0,9.0,9.0,22,0.68 +22584,81.0,8.0,8.0,9.0,8.0,9.0,8.0,20,0.63 +14097,86.0,9.0,9.0,10.0,10.0,9.0,9.0,7,0.22 +2958,89.0,9.0,9.0,10.0,10.0,10.0,10.0,7,0.24 +36084,79.0,8.0,8.0,8.0,9.0,8.0,8.0,32,0.98 +14540,87.0,9.0,9.0,9.0,9.0,9.0,9.0,45,1.38 +61169,83.0,9.0,8.0,9.0,9.0,8.0,9.0,35,1.06 +74666,86.0,9.0,9.0,9.0,9.0,9.0,9.0,29,0.88 +53873,86.0,9.0,8.0,8.0,8.0,9.0,9.0,37,1.13 +31765,,,,,,,,0, +36784,,,,,,,,0, +76760,98.0,10.0,10.0,10.0,10.0,9.0,10.0,23,0.7 +62456,,,,,,,,1,0.03 +25068,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.12 +54683,91.0,10.0,10.0,10.0,10.0,9.0,9.0,39,1.43 +9198,,,,,,,,0, +5854,97.0,10.0,9.0,10.0,10.0,10.0,10.0,59,1.78 +31268,,,,,,,,0, +45788,,,,,,,,0, +53685,99.0,10.0,10.0,10.0,10.0,9.0,9.0,15,0.48 +70575,73.0,8.0,8.0,9.0,9.0,9.0,8.0,176,5.46 +26419,95.0,10.0,10.0,10.0,10.0,10.0,9.0,82,2.6 +41045,81.0,9.0,7.0,10.0,9.0,9.0,9.0,112,3.4 +21123,96.0,10.0,9.0,10.0,10.0,10.0,9.0,49,1.5 +44688,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.03 +23800,,,,,,,,0, +65131,93.0,9.0,9.0,9.0,9.0,10.0,10.0,3,0.09 +71003,70.0,8.0,7.0,9.0,8.0,9.0,9.0,3,0.11 +31288,,,,,,,,0, +6534,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +13385,93.0,10.0,9.0,10.0,10.0,8.0,10.0,15,0.48 +49591,86.0,9.0,9.0,10.0,10.0,10.0,9.0,31,0.98 +50557,75.0,8.0,8.0,9.0,9.0,9.0,8.0,186,5.65 +51399,91.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.23 +40893,,,,,,,,0, +50243,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.17 +14877,60.0,,,,,,,2,0.06 +76408,,,,,,,,0, +52577,,,,,,,,0, +21066,97.0,10.0,10.0,10.0,10.0,10.0,10.0,46,1.45 +16443,95.0,10.0,9.0,10.0,10.0,10.0,9.0,19,0.61 +2248,,,,,,,,1,0.03 +38092,70.0,8.0,6.0,10.0,10.0,9.0,9.0,2,0.08 +31383,92.0,9.0,9.0,10.0,10.0,10.0,9.0,241,7.37 +64720,90.0,9.0,9.0,10.0,10.0,10.0,9.0,170,5.23 +76087,100.0,10.0,10.0,10.0,10.0,9.0,10.0,49,1.53 +73604,80.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.03 +60302,80.0,10.0,8.0,4.0,9.0,10.0,8.0,2,0.38 +59603,,,,,,,,0, +12036,77.0,8.0,7.0,9.0,9.0,9.0,8.0,6,0.19 +65348,,,,,,,,0, +44130,92.0,10.0,9.0,10.0,10.0,10.0,10.0,13,0.41 +1849,98.0,10.0,10.0,10.0,10.0,10.0,10.0,50,1.63 +55666,,,,,,,,0, +53304,,,,,,,,0, +65747,93.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.21 +54083,,,,,,,,0, +9053,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +64103,,,,,,,,0, +37688,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.29 +59659,,,,,,,,0, +52304,96.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.44 +6694,90.0,9.0,9.0,10.0,10.0,7.0,9.0,4,0.14 +17436,80.0,10.0,10.0,10.0,8.0,10.0,10.0,1,0.04 +4452,,,,,,,,0, +30761,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.11 +72734,,,,,,,,0, +43084,,,,,,,,0, +30147,,,,,,,,0, +23403,,,,,,,,0, +73054,87.0,9.0,8.0,10.0,10.0,9.0,9.0,3,0.12 +15183,,,,,,,,0, +20291,98.0,10.0,10.0,10.0,10.0,9.0,10.0,10,0.75 +13503,88.0,8.0,9.0,10.0,9.0,9.0,9.0,5,0.16 +10262,93.0,10.0,9.0,10.0,10.0,9.0,9.0,58,1.83 +75613,,,,,,,,0, +7942,80.0,10.0,9.0,8.0,10.0,9.0,8.0,2,0.9 +63420,,,,,,,,0, +56902,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.09 +23979,,,,,,,,0, +8941,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.07 +75239,78.0,9.0,8.0,9.0,9.0,9.0,8.0,70,2.15 +66260,98.0,10.0,10.0,10.0,10.0,10.0,10.0,29,0.9 +4971,97.0,10.0,10.0,10.0,10.0,9.0,10.0,128,4.07 +68986,92.0,9.0,9.0,9.0,9.0,9.0,9.0,23,1.54 +1583,90.0,10.0,9.0,7.0,10.0,8.0,10.0,4,0.12 +44287,96.0,10.0,10.0,10.0,10.0,10.0,10.0,113,3.5 +13188,,,,,,,,0, +13873,,,,,,,,0, +71980,80.0,9.0,8.0,10.0,10.0,7.0,9.0,2,0.06 +71933,82.0,9.0,8.0,9.0,9.0,9.0,9.0,98,3.0 +28343,91.0,9.0,9.0,9.0,9.0,9.0,9.0,20,0.62 +53821,98.0,10.0,10.0,10.0,10.0,10.0,10.0,44,1.37 +4506,96.0,10.0,10.0,10.0,10.0,9.0,10.0,62,1.99 +19759,66.0,9.0,7.0,8.0,9.0,7.0,7.0,7,0.22 +49111,98.0,10.0,10.0,10.0,10.0,10.0,10.0,35,1.2 +76676,97.0,10.0,10.0,10.0,10.0,10.0,10.0,67,2.09 +61928,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +31324,60.0,10.0,10.0,10.0,8.0,7.0,7.0,2,0.07 +56039,90.0,9.0,9.0,10.0,10.0,10.0,9.0,132,4.03 +56109,85.0,9.0,9.0,10.0,10.0,9.0,9.0,25,0.76 +61957,75.0,10.0,10.0,6.0,7.0,10.0,8.0,4,0.13 +62457,99.0,10.0,10.0,10.0,10.0,10.0,10.0,75,2.36 +35629,78.0,9.0,9.0,9.0,9.0,10.0,8.0,9,0.28 +55245,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +74987,97.0,10.0,10.0,10.0,10.0,10.0,9.0,38,1.18 +24050,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +43139,,,,,,,,0, +76599,80.0,9.0,9.0,7.0,10.0,10.0,9.0,7,0.27 +29663,89.0,9.0,9.0,10.0,10.0,10.0,9.0,28,0.87 +21646,85.0,9.0,8.0,10.0,10.0,10.0,9.0,39,1.2 +26517,87.0,9.0,9.0,10.0,9.0,10.0,9.0,30,0.93 +71674,,,,,,,,0, +73688,83.0,9.0,8.0,10.0,10.0,10.0,9.0,26,0.83 +25342,87.0,10.0,9.0,9.0,10.0,10.0,9.0,6,0.34 +58003,76.0,8.0,8.0,9.0,9.0,10.0,8.0,9,0.28 +65554,98.0,10.0,10.0,10.0,10.0,10.0,10.0,64,2.0 +16360,94.0,10.0,9.0,10.0,10.0,9.0,10.0,81,2.53 +19510,99.0,10.0,10.0,10.0,10.0,10.0,10.0,74,2.37 +7188,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.5 +79,90.0,9.0,8.0,9.0,9.0,9.0,9.0,14,0.6 +56586,70.0,9.0,10.0,9.0,10.0,9.0,9.0,2,1.03 +44773,,,,,,,,1,0.03 +54071,,,,,,,,0, +50166,,,,,,,,0, +18501,100.0,9.0,9.0,10.0,10.0,9.0,9.0,5,0.17 +75362,,,,,,,,0, +67653,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.1 +46676,,,,,,,,0, +40592,96.0,10.0,10.0,10.0,10.0,10.0,10.0,169,5.23 +60283,,,,,,,,0, +39154,93.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.1 +59393,89.0,9.0,9.0,9.0,9.0,10.0,9.0,16,0.51 +31244,99.0,10.0,10.0,10.0,10.0,10.0,10.0,73,2.25 +3581,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.13 +13206,67.0,8.0,7.0,7.0,7.0,9.0,8.0,9,0.31 +32741,94.0,10.0,10.0,10.0,10.0,10.0,9.0,23,0.8 +33804,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.13 +59306,93.0,10.0,9.0,10.0,10.0,10.0,10.0,30,0.92 +66862,94.0,10.0,10.0,10.0,10.0,10.0,9.0,117,3.72 +55114,82.0,10.0,9.0,9.0,10.0,10.0,9.0,9,0.44 +66231,,,,,,,,0, +31475,93.0,10.0,10.0,10.0,10.0,9.0,10.0,61,1.92 +27322,,,,,,,,0, +45680,90.0,9.0,9.0,10.0,9.0,10.0,9.0,26,0.8 +40711,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.06 +3276,97.0,10.0,10.0,10.0,10.0,9.0,10.0,62,2.01 +71266,95.0,10.0,10.0,10.0,10.0,9.0,9.0,41,1.29 +63402,82.0,8.0,9.0,9.0,9.0,10.0,8.0,11,0.43 +71019,95.0,10.0,10.0,10.0,10.0,9.0,10.0,51,1.7 +5603,99.0,10.0,10.0,10.0,10.0,10.0,10.0,28,0.93 +64402,,,,,,,,0, +10217,98.0,10.0,9.0,10.0,10.0,9.0,10.0,49,1.54 +52571,96.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.17 +15480,,,,,,,,0, +21849,,,,,,,,0, +63382,,,,,,,,0, +41885,93.0,9.0,8.0,10.0,10.0,10.0,9.0,3,0.11 +25770,100.0,10.0,10.0,10.0,10.0,10.0,10.0,27,0.92 +40324,97.0,10.0,10.0,10.0,10.0,10.0,10.0,45,1.4 +28740,98.0,10.0,10.0,10.0,10.0,10.0,10.0,73,2.34 +43690,84.0,9.0,9.0,9.0,10.0,10.0,8.0,31,1.05 +61493,98.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.51 +36509,94.0,10.0,10.0,10.0,10.0,10.0,9.0,34,1.07 +70632,,,,,,,,1,0.03 +6262,100.0,9.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +28415,,,,,,,,0, +40713,99.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.64 +75712,,,,,,,,0, +49864,98.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.37 +44510,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.03 +12013,93.0,10.0,10.0,10.0,10.0,10.0,9.0,63,2.02 +65981,80.0,9.0,8.0,9.0,9.0,9.0,8.0,102,3.27 +38539,98.0,10.0,10.0,10.0,10.0,10.0,10.0,47,1.59 +33966,98.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.78 +6539,70.0,7.0,9.0,7.0,6.0,10.0,8.0,2,0.09 +1000,90.0,9.0,9.0,10.0,10.0,10.0,9.0,23,0.73 +41470,92.0,10.0,10.0,9.0,10.0,10.0,9.0,56,1.75 +35698,91.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.22 +50920,,,,,,,,0, +56524,,,,,,,,0, +56655,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.61 +69613,96.0,10.0,10.0,10.0,10.0,10.0,10.0,55,1.96 +1682,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.16 +30569,,,,,,,,0, +45425,,,,,,,,0, +44968,98.0,10.0,10.0,10.0,10.0,10.0,10.0,34,1.1 +56322,,,,,,,,4,0.13 +33099,93.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.1 +52607,84.0,10.0,7.0,10.0,10.0,10.0,8.0,8,0.25 +28105,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.34 +17990,98.0,10.0,10.0,10.0,10.0,10.0,10.0,49,1.57 +70525,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.23 +3612,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +38144,,,,,,,,0, +49943,98.0,10.0,10.0,10.0,10.0,10.0,9.0,21,0.66 +14743,,,,,,,,0, +50515,,,,,,,,0, +21601,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.14 +41894,,,,,,,,0, +45202,,,,,,,,0, +53997,96.0,10.0,10.0,10.0,10.0,10.0,9.0,67,2.1 +48159,96.0,10.0,10.0,10.0,10.0,10.0,9.0,65,2.04 +67610,99.0,10.0,10.0,10.0,10.0,10.0,10.0,55,1.77 +4010,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.42 +14014,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.17 +74158,80.0,9.0,7.0,7.0,9.0,8.0,8.0,5,0.16 +71732,98.0,10.0,10.0,10.0,10.0,10.0,10.0,41,1.28 +40911,97.0,10.0,10.0,10.0,10.0,10.0,10.0,30,0.96 +46736,97.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.84 +31579,87.0,9.0,9.0,9.0,9.0,9.0,9.0,59,1.82 +5370,80.0,9.0,9.0,9.0,9.0,10.0,10.0,4,0.13 +62398,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.17 +18008,97.0,10.0,10.0,10.0,10.0,9.0,10.0,45,1.42 +28110,98.0,10.0,10.0,10.0,10.0,10.0,9.0,41,1.29 +57567,20.0,2.0,2.0,2.0,2.0,2.0,2.0,1,0.03 +62393,93.0,9.0,9.0,10.0,10.0,9.0,9.0,16,0.5 +44530,97.0,10.0,10.0,10.0,10.0,10.0,10.0,77,2.41 +47278,97.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.3 +73050,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.57 +72723,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +2073,60.0,6.0,2.0,8.0,8.0,6.0,6.0,1,0.03 +26606,99.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.46 +76692,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.09 +3341,93.0,9.0,9.0,9.0,9.0,9.0,8.0,12,0.47 +37300,91.0,10.0,9.0,9.0,9.0,10.0,9.0,21,0.68 +70755,,,,,,,,0, +72935,91.0,10.0,9.0,10.0,10.0,10.0,9.0,34,1.06 +1096,,,,,,,,0, +64198,94.0,10.0,9.0,10.0,10.0,10.0,9.0,71,2.48 +11412,94.0,10.0,9.0,10.0,10.0,10.0,10.0,133,4.36 +31374,,,,,,,,0, +53607,93.0,9.0,9.0,10.0,10.0,9.0,9.0,11,0.35 +59644,91.0,10.0,9.0,9.0,9.0,9.0,9.0,14,0.47 +55621,97.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.51 +19260,,,,,,,,0, +35008,,,,,,,,0, +15017,99.0,10.0,10.0,10.0,10.0,10.0,10.0,23,0.72 +53712,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.35 +56353,,,,,,,,0, +70538,90.0,9.0,10.0,9.0,9.0,9.0,9.0,68,2.13 +61510,94.0,10.0,9.0,10.0,10.0,9.0,10.0,74,2.37 +67776,96.0,10.0,10.0,10.0,10.0,9.0,9.0,35,1.1 +12252,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +1286,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.38 +71094,95.0,10.0,9.0,10.0,10.0,10.0,9.0,24,0.82 +40731,96.0,10.0,10.0,10.0,10.0,10.0,10.0,49,1.6 +46184,92.0,10.0,9.0,10.0,10.0,9.0,10.0,68,2.31 +55946,95.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.41 +31532,92.0,10.0,9.0,10.0,10.0,9.0,10.0,6,0.27 +42543,,,,,,,,0, +16491,,,,,,,,0, +69125,98.0,10.0,10.0,10.0,10.0,10.0,10.0,49,1.59 +37339,100.0,10.0,9.0,10.0,10.0,10.0,10.0,8,1.64 +8367,100.0,10.0,8.0,10.0,9.0,10.0,10.0,2,0.36 +10147,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.38 +63947,97.0,10.0,9.0,10.0,10.0,10.0,10.0,31,1.03 +74499,67.0,9.0,6.0,7.0,7.0,9.0,7.0,16,0.5 +41870,,,,,,,,0, +15529,89.0,9.0,9.0,10.0,10.0,10.0,9.0,62,1.94 +40090,99.0,10.0,9.0,10.0,10.0,9.0,10.0,25,0.95 +53245,98.0,10.0,10.0,10.0,10.0,9.0,10.0,74,2.37 +54623,86.0,9.0,10.0,9.0,9.0,10.0,9.0,14,0.64 +44684,80.0,8.0,7.0,7.0,9.0,9.0,7.0,3,0.18 +23439,100.0,10.0,10.0,10.0,10.0,10.0,10.0,69,3.3 +49644,80.0,,,,,,,1,0.03 +51738,,,,,,,,0, +23905,93.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.1 +43167,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.13 +32575,95.0,10.0,10.0,9.0,10.0,9.0,10.0,4,0.13 +6031,92.0,9.0,9.0,9.0,9.0,9.0,9.0,14,0.72 +44353,92.0,10.0,9.0,10.0,10.0,9.0,9.0,24,0.78 +69425,93.0,10.0,9.0,10.0,10.0,10.0,10.0,21,0.66 +23666,,,,,,,,0, +32974,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.2 +12574,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.25 +64284,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.04 +39670,86.0,9.0,9.0,9.0,9.0,9.0,9.0,50,1.64 +2348,97.0,10.0,10.0,10.0,10.0,10.0,9.0,20,0.68 +77019,,,,,,,,0, +22925,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +16363,97.0,10.0,10.0,10.0,10.0,10.0,10.0,35,1.86 +67952,,,,,,,,0, +59415,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.06 +68114,,,,,,,,0, +44335,97.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.22 +54996,96.0,10.0,10.0,10.0,10.0,9.0,9.0,12,0.38 +59457,86.0,9.0,9.0,9.0,10.0,10.0,9.0,7,0.23 +56259,98.0,10.0,10.0,10.0,10.0,10.0,9.0,8,0.27 +64772,98.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.65 +19498,,,,,,,,0, +71848,98.0,10.0,10.0,10.0,10.0,10.0,10.0,24,0.77 +60501,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +17450,100.0,10.0,10.0,8.0,10.0,8.0,10.0,1,0.03 +7133,,,,,,,,0, +34382,100.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.3 +697,,,,,,,,1,0.04 +35383,95.0,10.0,9.0,10.0,10.0,9.0,10.0,29,1.45 +4498,92.0,10.0,10.0,10.0,10.0,10.0,10.0,48,1.53 +35348,80.0,8.0,7.0,10.0,8.0,8.0,8.0,5,0.17 +49440,84.0,10.0,9.0,9.0,9.0,10.0,9.0,14,0.45 +8631,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.23 +75740,,,,,,,,0, +40781,84.0,9.0,8.0,9.0,9.0,9.0,9.0,95,3.04 +8486,,,,,,,,0, +56162,20.0,6.0,6.0,8.0,2.0,4.0,4.0,1,0.03 +22639,95.0,10.0,9.0,10.0,10.0,10.0,9.0,45,1.5 +46125,88.0,9.0,9.0,9.0,9.0,10.0,9.0,10,0.32 +26406,93.0,10.0,9.0,10.0,10.0,10.0,9.0,9,1.24 +74140,,,,,,,,0, +33237,98.0,10.0,10.0,10.0,10.0,9.0,9.0,27,1.04 +75951,88.0,10.0,10.0,9.0,10.0,10.0,9.0,6,0.19 +19180,88.0,10.0,9.0,9.0,10.0,9.0,9.0,16,0.53 +70817,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.21 +3479,96.0,10.0,10.0,10.0,10.0,9.0,10.0,28,0.9 +33150,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +45462,,,,,,,,0, +58224,,,,,,,,0, +32263,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +77043,92.0,10.0,9.0,10.0,10.0,9.0,9.0,11,0.35 +47008,,,,,,,,0, +29708,95.0,10.0,10.0,10.0,10.0,9.0,9.0,87,2.76 +63429,96.0,10.0,9.0,10.0,10.0,9.0,9.0,113,3.63 +61611,98.0,10.0,10.0,10.0,10.0,9.0,10.0,71,2.57 +4028,99.0,10.0,10.0,10.0,10.0,10.0,10.0,24,0.77 +33653,99.0,10.0,10.0,10.0,10.0,9.0,10.0,53,1.68 +42269,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.1 +12077,100.0,10.0,10.0,10.0,10.0,10.0,10.0,32,1.05 +75046,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.34 +52065,76.0,8.0,7.0,8.0,10.0,8.0,8.0,5,0.17 +18322,92.0,10.0,9.0,10.0,10.0,10.0,9.0,10,0.33 +5313,96.0,10.0,10.0,10.0,10.0,9.0,10.0,52,1.68 +59891,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.34 +65100,96.0,10.0,10.0,10.0,10.0,10.0,10.0,23,0.74 +62247,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.22 +1201,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,0.85 +39742,,,,,,,,0, +50781,97.0,10.0,10.0,10.0,10.0,10.0,10.0,37,2.08 +57146,95.0,10.0,10.0,10.0,10.0,10.0,10.0,30,0.97 +20370,91.0,10.0,9.0,10.0,10.0,10.0,9.0,13,0.42 +67586,99.0,10.0,10.0,10.0,10.0,10.0,10.0,27,0.99 +47961,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +69114,91.0,9.0,10.0,10.0,10.0,10.0,9.0,7,0.37 +44111,100.0,10.0,,10.0,10.0,10.0,10.0,1,0.04 +29587,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.26 +18696,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.3 +39207,,,,,,,,0, +5997,,,,,,,,0, +73585,87.0,10.0,9.0,10.0,10.0,9.0,9.0,22,0.73 +65044,91.0,10.0,8.0,10.0,10.0,10.0,9.0,23,0.78 +50826,,,,,,,,0, +21158,98.0,10.0,10.0,10.0,10.0,10.0,10.0,24,0.9 +66066,,,,,,,,0, +6634,,,,,,,,0, +62629,84.0,9.0,8.0,10.0,9.0,9.0,9.0,32,1.03 +68892,90.0,10.0,9.0,10.0,10.0,9.0,9.0,8,0.8 +34136,95.0,10.0,10.0,10.0,10.0,9.0,9.0,16,0.51 +49574,94.0,10.0,10.0,10.0,10.0,9.0,10.0,10,0.33 +41910,96.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.22 +23695,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.21 +56216,95.0,9.0,9.0,10.0,10.0,10.0,9.0,52,1.65 +37012,,,,,,,,0, +31959,,,,,,,,0, +9632,,,,,,,,1,0.03 +47445,,,,,,,,0, +33992,93.0,9.0,10.0,10.0,10.0,9.0,9.0,23,0.82 +31688,,,,,,,,0, +47713,92.0,10.0,9.0,10.0,10.0,9.0,9.0,20,0.75 +49027,50.0,2.0,2.0,2.0,10.0,9.0,4.0,2,0.07 +26978,100.0,10.0,10.0,9.0,10.0,9.0,10.0,8,0.26 +35795,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +8733,,,,,,,,2,0.06 +7375,,,,,,,,0, +15261,88.0,9.0,7.0,9.0,10.0,9.0,8.0,11,0.36 +71669,92.0,9.0,8.0,10.0,10.0,9.0,9.0,15,0.53 +24734,,,,,,,,0, +66499,97.0,10.0,10.0,10.0,10.0,9.0,10.0,87,2.83 +64956,,,,,,,,0, +24213,,,,,,,,0, +34772,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +7675,95.0,10.0,10.0,10.0,10.0,9.0,9.0,16,0.54 +1581,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +60264,99.0,10.0,10.0,10.0,10.0,9.0,10.0,37,1.2 +43831,,,,,,,,0, +69423,96.0,10.0,10.0,10.0,10.0,10.0,9.0,90,2.94 +76196,88.0,9.0,9.0,10.0,10.0,9.0,9.0,26,0.86 +41403,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +16134,92.0,10.0,10.0,9.0,10.0,10.0,9.0,18,0.59 +13214,83.0,8.0,9.0,9.0,9.0,9.0,8.0,6,0.19 +70378,98.0,10.0,10.0,10.0,10.0,10.0,10.0,97,3.13 +61695,90.0,10.0,10.0,10.0,10.0,9.0,9.0,7,0.23 +91,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.1 +18709,96.0,10.0,10.0,10.0,10.0,10.0,10.0,53,1.7 +2099,90.0,10.0,10.0,10.0,10.0,8.0,8.0,2,0.07 +56158,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.32 +64880,92.0,9.0,9.0,10.0,9.0,10.0,10.0,66,2.39 +67950,84.0,10.0,9.0,9.0,10.0,10.0,8.0,5,0.17 +29579,,,,,,,,1,0.03 +41312,96.0,10.0,9.0,10.0,10.0,10.0,10.0,23,0.8 +1254,95.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.95 +57466,96.0,10.0,10.0,10.0,10.0,9.0,10.0,55,1.78 +35757,97.0,10.0,10.0,10.0,10.0,9.0,10.0,26,0.95 +45767,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +76846,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +12146,,,,,,,,0, +71208,,,,,,,,0, +72193,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.29 +58935,90.0,8.0,10.0,9.0,9.0,9.0,9.0,5,0.17 +11451,85.0,9.0,10.0,10.0,10.0,8.0,8.0,5,0.17 +5312,87.0,9.0,9.0,9.0,10.0,10.0,9.0,25,1.06 +27244,,,,,,,,0, +40667,,,,,,,,0, +1898,,,,,,,,0, +69713,79.0,9.0,10.0,9.0,9.0,8.0,8.0,17,0.59 +66905,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +42546,80.0,8.0,9.0,8.0,9.0,8.0,8.0,5,0.17 +16657,100.0,10.0,10.0,10.0,10.0,10.0,10.0,38,1.26 +13163,89.0,9.0,9.0,7.0,10.0,9.0,9.0,7,0.63 +1172,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +5119,96.0,9.0,10.0,10.0,10.0,9.0,9.0,14,0.47 +29310,100.0,8.0,8.0,8.0,8.0,8.0,8.0,1,0.03 +8431,73.0,7.0,9.0,9.0,9.0,9.0,8.0,3,0.12 +67423,,,,,,,,0, +64436,80.0,10.0,7.0,10.0,10.0,9.0,9.0,2,0.07 +76573,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +2938,,,,,,,,0, +16651,80.0,9.0,7.0,10.0,9.0,6.0,7.0,3,0.1 +54099,,,,,,,,0, +36435,96.0,10.0,9.0,10.0,10.0,10.0,10.0,63,2.03 +19082,96.0,10.0,10.0,10.0,10.0,10.0,10.0,23,0.76 +37253,,,,,,,,0, +2180,,,,,,,,0, +620,96.0,10.0,9.0,10.0,10.0,9.0,9.0,19,0.65 +59340,89.0,9.0,9.0,10.0,10.0,9.0,9.0,24,0.77 +62832,,,,,,,,0, +27889,,,,,,,,0, +33261,,,,,,,,0, +53328,90.0,8.0,9.0,9.0,9.0,9.0,9.0,2,0.07 +66317,91.0,9.0,9.0,10.0,9.0,10.0,9.0,70,2.39 +34654,80.0,8.0,10.0,10.0,9.0,9.0,8.0,2,0.07 +1788,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.42 +37565,98.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.62 +56849,,,,,,,,0, +38546,,,,,,,,0, +2136,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.39 +53709,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +31663,93.0,10.0,9.0,10.0,9.0,10.0,9.0,65,2.18 +1855,,,,,,,,1,0.03 +56634,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.1 +72390,,,,,,,,0, +54094,80.0,6.0,6.0,8.0,10.0,8.0,8.0,1,0.03 +9236,98.0,10.0,10.0,10.0,10.0,10.0,9.0,54,1.81 +29540,,,,,,,,0, +72302,88.0,10.0,10.0,10.0,10.0,9.0,9.0,19,0.83 +174,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.06 +61575,90.0,9.0,9.0,10.0,10.0,9.0,9.0,21,1.0 +76407,40.0,6.0,5.0,7.0,5.0,5.0,5.0,4,0.13 +20301,,,,,,,,0, +19822,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.07 +56926,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.1 +46198,,,,,,,,0, +75629,80.0,10.0,6.0,10.0,10.0,10.0,8.0,1,0.03 +50060,84.0,9.0,8.0,10.0,10.0,9.0,9.0,5,0.16 +1618,96.0,10.0,10.0,10.0,10.0,10.0,10.0,94,3.04 +966,80.0,10.0,8.0,6.0,6.0,10.0,6.0,1,0.22 +46515,96.0,10.0,10.0,10.0,10.0,10.0,10.0,80,2.67 +58871,85.0,9.0,9.0,8.0,9.0,9.0,8.0,11,0.6 +40788,100.0,10.0,10.0,10.0,10.0,10.0,9.0,10,0.33 +44686,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,0.1 +9401,95.0,10.0,10.0,10.0,10.0,9.0,10.0,75,2.45 +41015,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.2 +9641,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.53 +48113,91.0,10.0,9.0,10.0,10.0,9.0,9.0,32,1.13 +46144,92.0,10.0,9.0,10.0,10.0,10.0,9.0,15,1.05 +65680,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.13 +4508,98.0,10.0,10.0,10.0,10.0,9.0,10.0,33,1.27 +67383,,,,,,,,0, +10044,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +23082,,,,,,,,0, +46033,,,,,,,,0, +8438,95.0,10.0,9.0,9.0,10.0,10.0,9.0,122,4.12 +63924,97.0,10.0,10.0,10.0,10.0,10.0,9.0,27,0.99 +43245,,,,,,,,0, +12006,98.0,10.0,10.0,10.0,10.0,10.0,10.0,22,0.77 +47214,94.0,10.0,9.0,10.0,10.0,10.0,9.0,47,1.54 +61182,,,,,,,,0, +8636,89.0,9.0,9.0,9.0,10.0,10.0,9.0,37,1.21 +338,,,,,,,,1,0.03 +2420,83.0,9.0,9.0,10.0,9.0,10.0,9.0,47,1.58 +37584,,,,,,,,0, +15091,85.0,9.0,9.0,10.0,9.0,10.0,9.0,31,1.08 +71862,100.0,10.0,10.0,9.0,10.0,10.0,10.0,5,0.16 +70066,79.0,9.0,9.0,8.0,8.0,8.0,8.0,35,1.15 +7442,98.0,10.0,9.0,10.0,10.0,10.0,10.0,101,3.28 +29816,82.0,8.0,9.0,9.0,9.0,9.0,9.0,32,1.04 +58537,90.0,10.0,7.0,10.0,10.0,10.0,10.0,2,0.07 +36968,70.0,6.0,6.0,6.0,9.0,7.0,7.0,2,0.44 +26973,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +54136,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +34806,80.0,8.0,10.0,8.0,8.0,8.0,10.0,1,0.03 +14130,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.33 +73820,60.0,6.0,2.0,10.0,10.0,8.0,8.0,1,0.03 +43006,96.0,10.0,9.0,10.0,10.0,10.0,9.0,34,1.15 +55084,60.0,10.0,4.0,10.0,8.0,6.0,6.0,1,0.03 +57198,95.0,10.0,10.0,8.0,10.0,10.0,9.0,4,0.14 +52472,80.0,7.0,9.0,10.0,9.0,10.0,9.0,2,0.07 +32286,,,,,,,,0, +24352,,,,,,,,0, +35358,,,,,,,,0, +56194,,,,,,,,0, +44291,,,,,,,,0, +50142,80.0,10.0,9.0,9.0,9.0,10.0,9.0,6,0.2 +73838,85.0,9.0,9.0,9.0,10.0,9.0,9.0,23,0.75 +50913,91.0,10.0,10.0,10.0,10.0,10.0,9.0,63,2.1 +42992,,,,,,,,0, +4660,100.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.62 +13066,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.13 +420,95.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.44 +21435,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +37835,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.34 +70342,97.0,10.0,9.0,10.0,10.0,10.0,10.0,65,2.19 +57114,90.0,9.0,9.0,10.0,9.0,9.0,9.0,10,0.34 +71105,99.0,10.0,10.0,10.0,10.0,10.0,10.0,29,0.97 +52422,93.0,10.0,10.0,10.0,10.0,8.0,9.0,3,0.11 +17297,97.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.91 +38869,,,,,,,,0, +41180,91.0,9.0,10.0,10.0,10.0,10.0,10.0,7,0.23 +42682,,,,,,,,0, +7791,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +44525,,,,,,,,0, +70517,,,,,,,,0, +53466,96.0,10.0,10.0,10.0,10.0,9.0,10.0,22,0.73 +39568,90.0,9.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +26137,93.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.26 +24854,80.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.03 +37011,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.44 +70001,,,,,,,,0, +72194,97.0,10.0,10.0,10.0,10.0,9.0,9.0,21,0.71 +73209,,,,,,,,0, +12421,94.0,10.0,9.0,9.0,10.0,10.0,9.0,65,2.3 +19767,97.0,10.0,10.0,10.0,10.0,10.0,10.0,79,2.59 +46279,87.0,9.0,9.0,10.0,10.0,8.0,9.0,24,0.82 +50702,87.0,9.0,9.0,8.0,9.0,9.0,9.0,9,0.3 +3746,,,,,,,,0, +33645,40.0,6.0,2.0,8.0,6.0,8.0,,1,0.04 +46114,,,,,,,,0, +12410,,,,,,,,0, +76710,83.0,9.0,8.0,10.0,10.0,9.0,9.0,111,3.89 +58574,79.0,9.0,8.0,9.0,9.0,9.0,8.0,106,3.81 +68852,78.0,8.0,8.0,8.0,8.0,10.0,8.0,21,0.71 +57923,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.2 +50644,,,,,,,,0, +50669,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +15977,73.0,8.0,7.0,9.0,9.0,9.0,8.0,101,3.43 +17033,96.0,10.0,10.0,10.0,9.0,9.0,10.0,11,0.37 +30858,,,,,,,,0, +40186,92.0,9.0,9.0,10.0,10.0,10.0,9.0,13,0.43 +68642,,,,,,,,1,0.03 +46007,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.19 +3543,,,,,,,,0, +34637,96.0,10.0,9.0,9.0,9.0,10.0,10.0,5,0.17 +22066,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.98 +66764,,,,,,,,0, +1129,83.0,9.0,9.0,10.0,10.0,10.0,9.0,21,0.8 +21032,98.0,10.0,9.0,10.0,10.0,9.0,10.0,9,0.3 +33180,90.0,9.0,10.0,10.0,10.0,9.0,10.0,4,0.13 +51871,100.0,10.0,10.0,9.0,10.0,9.0,9.0,4,0.13 +45907,94.0,10.0,9.0,9.0,10.0,9.0,9.0,29,0.97 +58328,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +39780,94.0,9.0,9.0,10.0,10.0,9.0,9.0,10,0.39 +53511,96.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.52 +65307,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.37 +53767,88.0,9.0,8.0,10.0,10.0,10.0,9.0,24,0.84 +61137,,,,,,,,0, +724,87.0,9.0,10.0,10.0,10.0,9.0,9.0,3,0.1 +34011,90.0,10.0,10.0,10.0,10.0,10.0,9.0,3,3.0 +42207,80.0,10.0,10.0,10.0,10.0,8.0,8.0,1,1.0 +39574,,,,,,,,0, +47020,,,,,,,,0, +42920,,,,,,,,0, +36642,,,,,,,,0, +1973,100.0,9.0,10.0,10.0,10.0,10.0,9.0,2,1.22 +8892,,,,,,,,0, +75762,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +6019,80.0,8.0,6.0,10.0,10.0,10.0,8.0,2,0.07 +54878,,,,,,,,6,0.2 +66444,100.0,9.0,7.0,10.0,10.0,9.0,9.0,3,0.11 +75608,85.0,8.0,10.0,9.0,8.0,9.0,9.0,8,0.28 +13544,,,,,,,,0, +58325,95.0,10.0,9.0,10.0,10.0,9.0,10.0,11,0.37 +71375,98.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.86 +22547,95.0,10.0,8.0,10.0,10.0,9.0,10.0,11,0.37 +50921,,,,,,,,1,0.03 +36374,90.0,9.0,8.0,10.0,10.0,9.0,9.0,2,0.07 +48350,90.0,10.0,9.0,10.0,9.0,9.0,9.0,2,0.07 +39165,99.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.87 +59246,96.0,10.0,10.0,10.0,10.0,10.0,10.0,62,2.04 +8095,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.39 +4906,75.0,9.0,7.0,9.0,9.0,10.0,9.0,4,0.13 +59893,90.0,10.0,9.0,10.0,9.0,10.0,10.0,2,0.07 +68339,40.0,6.0,2.0,10.0,8.0,8.0,6.0,1,0.04 +70158,,,,,,,,0, +22822,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.07 +50363,88.0,9.0,9.0,9.0,9.0,8.0,9.0,50,1.69 +10944,,,,,,,,0, +75807,86.0,9.0,9.0,9.0,10.0,9.0,9.0,37,1.27 +64000,95.0,9.0,10.0,10.0,10.0,8.0,9.0,4,0.14 +37727,95.0,10.0,9.0,10.0,10.0,9.0,10.0,18,0.65 +21086,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.17 +65171,94.0,10.0,9.0,10.0,10.0,10.0,9.0,90,2.97 +51725,92.0,10.0,8.0,10.0,10.0,10.0,10.0,6,0.2 +282,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +53003,,,,,,,,0, +34217,98.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.37 +66485,94.0,10.0,9.0,10.0,10.0,10.0,9.0,51,1.75 +57562,95.0,10.0,10.0,9.0,10.0,10.0,10.0,38,1.29 +73502,93.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.31 +60950,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.3 +73713,92.0,10.0,9.0,10.0,10.0,8.0,10.0,5,0.17 +72617,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.11 +67112,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.24 +73898,97.0,10.0,10.0,10.0,10.0,9.0,10.0,12,0.4 +19964,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +52360,,,,,,,,0, +36605,84.0,8.0,7.0,9.0,9.0,10.0,9.0,29,0.99 +56731,,,,,,,,0, +49230,93.0,9.0,9.0,10.0,9.0,9.0,9.0,3,0.13 +75345,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +55633,95.0,10.0,10.0,10.0,10.0,10.0,9.0,9,0.3 +2770,,,,,,,,0, +54982,90.0,9.0,8.0,10.0,10.0,9.0,9.0,28,0.94 +27126,95.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.41 +73627,93.0,9.0,10.0,10.0,9.0,9.0,9.0,10,0.37 +2570,94.0,10.0,10.0,10.0,10.0,9.0,9.0,37,1.25 +44597,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +1049,90.0,10.0,9.0,10.0,10.0,9.0,10.0,6,0.21 +10754,60.0,5.0,5.0,10.0,8.0,8.0,5.0,2,0.07 +30,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +51826,95.0,10.0,10.0,10.0,10.0,9.0,9.0,19,0.65 +26080,97.0,10.0,9.0,10.0,10.0,10.0,9.0,20,0.68 +75106,97.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.06 +7358,,,,,,,,0, +6616,,,,,,,,0, +33644,90.0,10.0,10.0,10.0,10.0,10.0,8.0,2,0.11 +2513,80.0,9.0,7.0,10.0,10.0,10.0,10.0,3,0.1 +6869,89.0,9.0,9.0,8.0,8.0,10.0,9.0,31,1.04 +18950,,,,,,,,0, +30264,78.0,8.0,8.0,8.0,8.0,9.0,8.0,16,0.56 +41305,96.0,10.0,10.0,10.0,10.0,10.0,9.0,109,3.66 +97,,,,,,,,0, +65507,94.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.23 +12917,,,,,,,,0, +45546,100.0,9.0,10.0,10.0,10.0,10.0,9.0,6,0.2 +55988,,,,,,,,0, +7907,,,,,,,,0, +59355,,,,,,,,0, +44711,98.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.45 +37674,,,,,,,,0, +19653,90.0,9.0,9.0,9.0,9.0,9.0,9.0,144,4.79 +65374,95.0,10.0,10.0,9.0,10.0,10.0,10.0,8,0.27 +24478,,,,,,,,0, +20467,100.0,8.0,8.0,8.0,10.0,8.0,8.0,1,0.03 +23168,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.52 +33733,96.0,10.0,9.0,10.0,10.0,10.0,10.0,143,4.83 +49442,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +52274,97.0,10.0,10.0,10.0,10.0,10.0,10.0,39,1.32 +51121,98.0,10.0,10.0,10.0,10.0,10.0,10.0,72,2.43 +20004,96.0,10.0,9.0,10.0,10.0,10.0,10.0,16,0.56 +64179,95.0,10.0,10.0,10.0,10.0,10.0,10.0,34,1.19 +55793,93.0,10.0,10.0,10.0,10.0,9.0,9.0,34,1.14 +10576,,,,,,,,0, +4324,93.0,10.0,8.0,9.0,10.0,10.0,9.0,9,0.3 +9208,,,,,,,,0, +26909,96.0,9.0,10.0,10.0,9.0,10.0,9.0,21,0.89 +21031,93.0,10.0,10.0,10.0,10.0,9.0,9.0,13,0.43 +30845,,,,,,,,0, +75876,,,,,,,,0, +17272,80.0,9.0,8.0,10.0,9.0,8.0,8.0,3,0.1 +33516,,,,,,,,0, +61497,80.0,6.0,6.0,8.0,8.0,8.0,8.0,1,0.04 +21723,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.1 +11762,70.0,8.0,9.0,6.0,6.0,9.0,8.0,2,0.13 +43714,97.0,10.0,9.0,10.0,10.0,9.0,9.0,13,0.46 +15774,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.34 +58567,,,,,,,,0, +28443,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.14 +47749,90.0,9.0,9.0,9.0,10.0,9.0,9.0,40,1.45 +40246,90.0,9.0,9.0,10.0,9.0,9.0,9.0,35,1.24 +73501,,,,,,,,0, +40157,89.0,9.0,9.0,9.0,10.0,10.0,9.0,32,1.07 +14748,,,,,,,,3,0.1 +8007,94.0,9.0,10.0,9.0,9.0,10.0,9.0,13,0.49 +43054,90.0,10.0,8.0,10.0,9.0,8.0,9.0,6,0.21 +3635,99.0,10.0,10.0,10.0,10.0,10.0,10.0,32,1.11 +15312,,,,,,,,0, +57975,96.0,10.0,10.0,10.0,10.0,10.0,9.0,40,1.47 +73381,96.0,9.0,10.0,10.0,10.0,10.0,10.0,6,0.2 +8529,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.1 +67802,89.0,10.0,8.0,10.0,10.0,9.0,9.0,8,0.34 +23122,,,,,,,,0, +23524,,,,,,,,0, +44505,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +50932,,,,,,,,0, +32163,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.21 +53705,88.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.17 +21,93.0,9.0,9.0,9.0,10.0,8.0,9.0,5,0.17 +50825,67.0,5.0,7.0,7.0,8.0,8.0,7.0,3,0.11 +21920,97.0,10.0,10.0,10.0,10.0,10.0,9.0,15,0.55 +17633,94.0,10.0,10.0,10.0,10.0,10.0,9.0,72,2.45 +25871,100.0,10.0,10.0,10.0,10.0,9.0,10.0,21,0.7 +43974,,,,,,,,0, +1494,97.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.44 +18838,83.0,9.0,10.0,9.0,9.0,8.0,8.0,7,0.25 +61114,,,,,,,,0, +3068,90.0,10.0,10.0,9.0,8.0,8.0,9.0,4,0.14 +33297,77.0,9.0,9.0,8.0,7.0,10.0,8.0,7,0.24 +11258,,,,,,,,0, +38691,96.0,10.0,10.0,9.0,10.0,10.0,9.0,9,0.31 +15949,90.0,10.0,10.0,10.0,10.0,9.0,8.0,2,0.27 +10248,,,,,,,,0, +618,92.0,10.0,10.0,9.0,9.0,10.0,9.0,15,0.51 +7885,82.0,9.0,9.0,8.0,7.0,10.0,8.0,13,0.45 +28461,91.0,9.0,9.0,9.0,9.0,10.0,9.0,13,0.44 +20310,,,,,,,,0, +3367,86.0,9.0,8.0,9.0,9.0,10.0,9.0,48,1.61 +59162,96.0,10.0,10.0,10.0,10.0,10.0,10.0,45,1.61 +25508,98.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.73 +14455,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +75417,94.0,10.0,9.0,9.0,9.0,9.0,9.0,20,0.68 +21559,,,,,,,,0, +51912,,,,,,,,0, +15709,,,,,,,,0, +8802,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +33588,97.0,9.0,10.0,10.0,10.0,10.0,10.0,13,0.52 +76259,72.0,7.0,7.0,8.0,9.0,8.0,8.0,10,0.34 +46452,90.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.07 +41376,97.0,10.0,9.0,10.0,10.0,9.0,9.0,58,1.96 +34063,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +8950,93.0,9.0,9.0,9.0,10.0,9.0,8.0,3,0.16 +28678,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.03 +45736,98.0,10.0,10.0,10.0,10.0,10.0,10.0,21,0.77 +26595,94.0,10.0,10.0,10.0,10.0,10.0,9.0,10,0.35 +53112,,,,,,,,0, +22757,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.24 +55660,100.0,10.0,10.0,10.0,10.0,10.0,10.0,33,1.11 +1321,,,,,,,,0, +33021,99.0,10.0,10.0,10.0,10.0,10.0,10.0,22,0.83 +24697,99.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.6 +6011,89.0,8.0,8.0,9.0,9.0,9.0,9.0,7,0.25 +61742,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +2069,88.0,9.0,10.0,9.0,9.0,9.0,9.0,16,0.9 +68161,80.0,8.0,10.0,9.0,10.0,8.0,9.0,2,0.07 +46372,93.0,9.0,8.0,9.0,9.0,10.0,10.0,3,0.11 +8437,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +15011,,,,,,,,0, +15186,71.0,8.0,8.0,9.0,8.0,9.0,7.0,7,0.24 +72679,,,,,,,,0, +64830,100.0,10.0,10.0,10.0,10.0,10.0,10.0,46,1.7 +57743,,,,,,,,0, +51268,,,,,,,,0, +9881,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +24238,20.0,2.0,2.0,2.0,2.0,,,4,0.14 +64741,,,,,,,,0, +51391,100.0,10.0,9.0,10.0,10.0,8.0,9.0,2,0.07 +55716,99.0,10.0,10.0,10.0,10.0,9.0,10.0,67,2.28 +41788,98.0,10.0,10.0,10.0,10.0,9.0,10.0,10,0.41 +56982,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.11 +29933,100.0,10.0,10.0,10.0,8.0,8.0,10.0,2,0.07 +5663,90.0,9.0,10.0,10.0,10.0,9.0,9.0,2,0.07 +30438,89.0,9.0,9.0,9.0,10.0,10.0,9.0,17,0.62 +60909,95.0,10.0,10.0,10.0,10.0,9.0,10.0,21,0.74 +58149,92.0,10.0,9.0,10.0,10.0,10.0,9.0,172,5.8 +60037,100.0,10.0,9.0,10.0,10.0,9.0,10.0,2,0.51 +63515,94.0,9.0,8.0,10.0,10.0,10.0,9.0,35,1.25 +59254,99.0,10.0,10.0,10.0,10.0,10.0,10.0,90,3.06 +47057,94.0,10.0,9.0,10.0,10.0,9.0,10.0,7,0.24 +3695,96.0,10.0,9.0,10.0,10.0,10.0,9.0,10,0.4 +44605,60.0,8.0,4.0,2.0,8.0,10.0,6.0,1,0.03 +58685,81.0,8.0,6.0,9.0,9.0,8.0,8.0,17,1.2 +58538,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +25636,82.0,9.0,7.0,9.0,9.0,10.0,10.0,10,0.35 +67793,,,,,,,,0, +43041,,,,,,,,0, +42677,97.0,10.0,9.0,10.0,10.0,8.0,10.0,6,0.21 +8583,,,,,,,,0, +3528,,,,,,,,0, +50013,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +50516,97.0,10.0,10.0,10.0,10.0,10.0,10.0,135,4.59 +33247,,,,,,,,0, +48595,,,,,,,,0, +17004,,,,,,,,0, +27634,,,,,,,,0, +15936,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.18 +60099,91.0,9.0,9.0,10.0,10.0,10.0,9.0,53,1.83 +28595,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.25 +47903,96.0,10.0,10.0,10.0,10.0,10.0,9.0,66,2.42 +48716,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +46475,97.0,10.0,10.0,10.0,10.0,10.0,10.0,85,2.98 +1198,88.0,10.0,10.0,9.0,9.0,9.0,9.0,6,0.22 +1226,93.0,10.0,9.0,10.0,10.0,9.0,10.0,7,0.24 +70806,92.0,10.0,9.0,9.0,10.0,9.0,9.0,18,0.61 +8157,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.07 +15514,,,,,,,,0, +1944,80.0,9.0,7.0,9.0,10.0,8.0,8.0,3,0.13 +23011,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +17640,90.0,10.0,9.0,10.0,10.0,10.0,9.0,5,0.17 +52347,89.0,9.0,9.0,9.0,9.0,9.0,9.0,32,1.18 +40684,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +25185,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.36 +39544,90.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.1 +38355,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.28 +72113,98.0,10.0,10.0,10.0,10.0,9.0,10.0,106,3.67 +28948,,,,,,,,0, +11454,80.0,8.0,8.0,10.0,8.0,10.0,6.0,2,0.07 +457,96.0,10.0,10.0,10.0,10.0,9.0,9.0,20,0.79 +73159,99.0,10.0,10.0,10.0,10.0,10.0,10.0,164,5.62 +27698,98.0,9.0,10.0,10.0,10.0,10.0,10.0,10,0.38 +4248,90.0,10.0,9.0,10.0,9.0,10.0,9.0,269,10.19 +70189,,,,,,,,0, +280,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.15 +58969,97.0,10.0,10.0,10.0,10.0,9.0,10.0,125,4.75 +38933,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +24345,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +23396,78.0,9.0,8.0,8.0,9.0,10.0,8.0,11,0.43 +15731,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +53465,95.0,10.0,10.0,10.0,10.0,10.0,10.0,20,1.04 +40549,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.28 +73908,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.66 +8919,,,,,,,,0, +15263,84.0,9.0,10.0,9.0,9.0,10.0,8.0,5,0.17 +57028,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.07 +14298,97.0,10.0,10.0,10.0,10.0,9.0,10.0,28,0.98 +22174,83.0,9.0,8.0,9.0,9.0,9.0,9.0,98,3.33 +67575,,,,,,,,0, +70452,99.0,10.0,10.0,10.0,10.0,10.0,10.0,43,1.51 +55785,93.0,9.0,8.0,10.0,10.0,10.0,9.0,19,0.67 +20707,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.27 +1573,100.0,10.0,10.0,10.0,10.0,9.0,10.0,10,0.36 +34565,,,,,,,,0, +76920,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.41 +5090,40.0,2.0,2.0,6.0,4.0,8.0,2.0,1,0.04 +56476,84.0,10.0,8.0,10.0,10.0,10.0,8.0,8,0.31 +303,87.0,9.0,8.0,10.0,10.0,9.0,9.0,3,0.11 +20328,98.0,10.0,10.0,10.0,10.0,10.0,10.0,100,3.85 +5218,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.13 +23356,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.51 +72783,67.0,8.0,7.0,8.0,7.0,9.0,8.0,7,0.24 +69864,,,,,,,,0, +71041,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.03 +36973,80.0,7.0,9.0,10.0,10.0,9.0,8.0,2,0.07 +474,87.0,9.0,8.0,9.0,9.0,8.0,9.0,14,0.57 +76984,93.0,9.0,9.0,10.0,10.0,10.0,9.0,27,0.94 +66517,,,,,,,,0, +56784,93.0,10.0,9.0,10.0,10.0,10.0,10.0,10,0.39 +3948,80.0,10.0,8.0,8.0,8.0,8.0,8.0,1,0.04 +74533,99.0,10.0,10.0,10.0,10.0,9.0,9.0,18,0.66 +4082,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +57160,92.0,10.0,9.0,10.0,10.0,10.0,9.0,121,4.13 +8837,,,,,,,,0, +25160,91.0,9.0,9.0,10.0,10.0,10.0,9.0,156,5.53 +74672,60.0,8.0,5.0,8.0,8.0,5.0,6.0,3,0.1 +18913,,,,,,,,0, +9409,,,,,,,,0, +39078,93.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.13 +18189,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.11 +15916,,,,,,,,0, +55159,88.0,9.0,10.0,9.0,10.0,9.0,9.0,19,0.68 +57627,84.0,10.0,9.0,10.0,8.0,8.0,8.0,5,0.17 +34091,,,,,,,,0, +1316,95.0,10.0,10.0,10.0,10.0,10.0,10.0,133,4.58 +44159,,,,,,,,0, +3397,,,,,,,,0, +7470,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.13 +23671,,,,,,,,0, +58540,,,,,,,,0, +71215,,,,,,,,0, +38601,60.0,8.0,10.0,10.0,10.0,8.0,10.0,1,0.04 +27984,,,,,,,,0, +43238,,,,,,,,0, +43362,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.32 +31764,100.0,10.0,10.0,10.0,10.0,9.0,10.0,20,0.72 +104,99.0,10.0,9.0,10.0,10.0,10.0,10.0,20,0.72 +19245,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.17 +62983,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.03 +59204,96.0,10.0,10.0,10.0,10.0,9.0,10.0,191,6.56 +43164,99.0,10.0,10.0,10.0,10.0,9.0,10.0,148,5.18 +61037,90.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.07 +8163,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +38955,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +46244,96.0,9.0,9.0,10.0,10.0,9.0,9.0,14,0.54 +46453,99.0,10.0,10.0,10.0,10.0,10.0,10.0,67,3.34 +64732,98.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.32 +6980,96.0,10.0,10.0,10.0,10.0,10.0,9.0,58,2.01 +43604,98.0,10.0,10.0,10.0,10.0,10.0,9.0,20,0.69 +71398,90.0,10.0,8.0,10.0,10.0,10.0,10.0,4,0.14 +75725,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.18 +33836,20.0,2.0,2.0,6.0,2.0,2.0,2.0,1,0.06 +15622,,,,,,,,0, +65248,,,,,,,,0, +67915,95.0,10.0,9.0,10.0,10.0,10.0,9.0,25,0.88 +76,97.0,10.0,10.0,10.0,10.0,9.0,10.0,12,0.42 +73119,92.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.32 +53974,93.0,10.0,10.0,9.0,8.0,10.0,10.0,3,0.17 +65870,95.0,10.0,10.0,10.0,10.0,10.0,10.0,50,1.72 +30714,97.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.69 +60517,80.0,8.0,6.0,10.0,10.0,10.0,8.0,1,0.04 +53236,86.0,9.0,9.0,9.0,10.0,9.0,9.0,24,0.84 +28995,,,,,,,,1,0.03 +18129,,,,,,,,0, +72370,94.0,10.0,10.0,10.0,10.0,9.0,10.0,36,1.31 +28926,100.0,9.0,10.0,10.0,10.0,10.0,9.0,5,0.18 +63076,96.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.21 +57797,100.0,10.0,10.0,10.0,10.0,8.0,10.0,11,0.4 +12120,90.0,10.0,9.0,10.0,10.0,9.0,10.0,2,0.07 +12136,97.0,10.0,10.0,10.0,10.0,10.0,9.0,23,0.86 +65797,99.0,10.0,10.0,10.0,10.0,10.0,9.0,21,0.72 +38871,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +57661,90.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.11 +35365,92.0,10.0,10.0,10.0,10.0,8.0,8.0,5,0.17 +66474,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +41193,,,,,,,,0, +19166,92.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.42 +59838,92.0,9.0,9.0,9.0,9.0,10.0,9.0,21,0.75 +50629,87.0,9.0,9.0,9.0,9.0,10.0,9.0,13,0.52 +36627,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.4 +49491,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.74 +37778,,,,,,,,0, +39173,20.0,2.0,2.0,2.0,2.0,2.0,2.0,5,0.18 +33224,,,,,,,,0, +74651,,,,,,,,0, +19509,91.0,9.0,9.0,9.0,9.0,8.0,9.0,10,0.35 +36194,98.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.56 +26562,97.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.53 +36175,96.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.56 +54577,,,,,,,,0, +28468,80.0,10.0,8.0,8.0,10.0,10.0,8.0,1,0.04 +19762,,,,,,,,0, +35979,,,,,,,,1,0.04 +73772,88.0,9.0,9.0,9.0,9.0,9.0,9.0,25,0.9 +25792,,,,,,,,0, +21709,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +20704,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +59314,,,,,,,,0, +34429,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +46023,,,,,,,,0, +42831,91.0,9.0,9.0,8.0,10.0,9.0,8.0,26,0.95 +36846,95.0,10.0,9.0,9.0,9.0,10.0,10.0,28,0.96 +38406,98.0,10.0,10.0,10.0,10.0,10.0,10.0,31,1.09 +58914,97.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.38 +36537,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +74678,80.0,9.0,8.0,9.0,9.0,9.0,8.0,8,0.33 +71552,96.0,10.0,9.0,10.0,10.0,10.0,9.0,108,3.77 +20702,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.46 +27712,93.0,9.0,8.0,10.0,10.0,9.0,10.0,25,0.87 +42969,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.56 +61234,96.0,10.0,10.0,9.0,10.0,10.0,9.0,23,0.86 +74819,89.0,10.0,10.0,10.0,10.0,9.0,10.0,15,0.64 +13742,,,,,,,,1,0.03 +75618,,,,,,,,0, +22803,,,,,,,,0, +20586,,,,,,,,0, +56341,,,,,,,,0, +36884,,,,,,,,1,0.04 +7343,,,,,,,,0, +74085,97.0,10.0,9.0,10.0,10.0,10.0,10.0,60,2.09 +60291,97.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.46 +45930,80.0,8.0,10.0,8.0,10.0,8.0,10.0,2,1.54 +34956,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +43825,97.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.47 +29633,,,,,,,,0, +31144,93.0,10.0,10.0,10.0,10.0,9.0,9.0,15,0.54 +12290,94.0,10.0,9.0,10.0,10.0,10.0,10.0,34,1.19 +53703,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.11 +65733,96.0,10.0,10.0,10.0,10.0,10.0,10.0,27,1.0 +74565,91.0,10.0,9.0,10.0,10.0,9.0,10.0,13,0.52 +41213,,,,,,,,0, +12378,100.0,10.0,10.0,9.0,10.0,10.0,10.0,2,0.07 +948,95.0,10.0,10.0,10.0,10.0,9.0,9.0,21,0.74 +15643,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.03 +52758,98.0,10.0,10.0,10.0,10.0,9.0,10.0,41,1.45 +16970,90.0,10.0,9.0,9.0,10.0,9.0,9.0,8,0.29 +7159,97.0,10.0,10.0,10.0,10.0,10.0,10.0,83,2.89 +66293,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.09 +45318,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.15 +44710,100.0,10.0,10.0,10.0,10.0,9.0,10.0,23,0.91 +5656,98.0,10.0,10.0,9.0,10.0,9.0,10.0,10,0.57 +31702,93.0,10.0,8.0,10.0,10.0,10.0,9.0,7,0.27 +69873,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.18 +42923,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.18 +43337,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +31609,96.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.65 +19405,,,,,,,,0, +63139,96.0,10.0,10.0,10.0,10.0,9.0,9.0,14,1.31 +77069,80.0,10.0,8.0,10.0,6.0,9.0,7.0,2,0.07 +47028,76.0,8.0,8.0,8.0,8.0,9.0,8.0,45,1.6 +74290,100.0,10.0,10.0,10.0,10.0,9.0,10.0,20,0.7 +5991,80.0,8.0,8.0,9.0,9.0,7.0,6.0,3,3.0 +46172,98.0,10.0,10.0,10.0,10.0,10.0,10.0,43,1.5 +886,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.11 +53864,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +30445,82.0,9.0,9.0,8.0,9.0,10.0,9.0,41,1.44 +32402,99.0,10.0,10.0,10.0,10.0,10.0,10.0,28,1.67 +49948,,,,,,,,0, +32341,98.0,10.0,10.0,10.0,10.0,10.0,9.0,18,0.64 +23663,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.35 +1210,82.0,9.0,9.0,9.0,9.0,10.0,9.0,34,1.24 +59570,97.0,10.0,10.0,10.0,10.0,9.0,10.0,31,1.08 +51013,73.0,7.0,8.0,9.0,9.0,9.0,8.0,16,0.57 +16619,86.0,9.0,9.0,9.0,9.0,10.0,9.0,77,2.73 +55684,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.11 +65025,84.0,8.0,9.0,8.0,8.0,10.0,8.0,28,1.0 +8737,85.0,9.0,10.0,9.0,9.0,10.0,9.0,27,1.19 +5696,84.0,9.0,9.0,9.0,8.0,10.0,9.0,58,2.3 +55799,50.0,8.0,7.0,6.0,6.0,8.0,5.0,2,0.12 +23187,98.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.98 +66894,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +53322,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,0.97 +19846,95.0,10.0,10.0,9.0,10.0,9.0,9.0,104,3.74 +54208,93.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.41 +66409,100.0,9.0,10.0,10.0,10.0,9.0,10.0,3,0.16 +17967,93.0,9.0,9.0,10.0,10.0,10.0,9.0,19,0.73 +39201,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.3 +8220,80.0,8.0,8.0,9.0,9.0,8.0,8.0,26,0.92 +74492,86.0,9.0,9.0,9.0,9.0,9.0,9.0,93,3.33 +50676,96.0,10.0,10.0,9.0,10.0,9.0,9.0,73,2.61 +43440,94.0,10.0,9.0,10.0,10.0,10.0,9.0,63,2.24 +32333,100.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.69 +54413,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.14 +987,,,,,,,,0, +47303,96.0,10.0,10.0,9.0,10.0,9.0,9.0,46,1.65 +39356,100.0,10.0,8.0,10.0,10.0,8.0,8.0,3,0.15 +25388,94.0,9.0,9.0,10.0,10.0,9.0,9.0,13,0.46 +71121,86.0,9.0,8.0,9.0,9.0,9.0,9.0,12,0.42 +21790,,,,,,,,0, +12276,99.0,10.0,10.0,10.0,10.0,9.0,10.0,16,0.57 +41239,87.0,9.0,9.0,9.0,9.0,9.0,9.0,4,0.15 +72234,90.0,10.0,8.0,10.0,9.0,10.0,10.0,2,0.07 +27243,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.34 +1174,87.0,9.0,8.0,9.0,9.0,9.0,9.0,3,0.11 +17510,83.0,9.0,10.0,10.0,10.0,8.0,9.0,13,0.45 +5661,,,,,,,,0, +26830,90.0,10.0,9.0,10.0,10.0,10.0,9.0,76,2.64 +7927,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.43 +9153,83.0,10.0,8.0,9.0,9.0,10.0,9.0,21,0.74 +46099,98.0,10.0,10.0,10.0,10.0,10.0,9.0,12,0.44 +63353,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.43 +39069,91.0,9.0,9.0,10.0,10.0,9.0,9.0,18,0.65 +11558,96.0,10.0,10.0,10.0,10.0,10.0,10.0,39,1.37 +48017,,,,,,,,0, +60618,,,,,,,,0, +32736,92.0,10.0,9.0,9.0,10.0,9.0,9.0,27,0.96 +54373,,,,,,,,0, +26056,96.0,10.0,10.0,10.0,10.0,10.0,10.0,30,1.09 +11598,95.0,10.0,9.0,10.0,10.0,9.0,10.0,26,0.92 +24289,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.12 +3507,,,,,,,,0, +43328,98.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.5 +75964,,,,,,,,0, +40891,93.0,9.0,7.0,9.0,10.0,7.0,9.0,3,0.11 +59994,,,,,,,,0, +66753,90.0,9.0,8.0,10.0,10.0,10.0,10.0,2,0.07 +73306,,,,,,,,0, +42578,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +14309,100.0,10.0,10.0,10.0,10.0,10.0,9.0,14,0.71 +76544,82.0,9.0,9.0,10.0,9.0,10.0,8.0,45,1.66 +27087,91.0,9.0,9.0,9.0,9.0,9.0,9.0,39,1.42 +41526,,,,,,,,0, +2875,81.0,8.0,9.0,9.0,9.0,9.0,8.0,18,0.67 +36303,95.0,10.0,9.0,10.0,9.0,9.0,9.0,8,0.28 +69169,99.0,10.0,10.0,10.0,10.0,10.0,10.0,21,0.75 +33483,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +53179,98.0,10.0,9.0,10.0,10.0,9.0,9.0,9,0.32 +69311,76.0,8.0,10.0,10.0,10.0,8.0,8.0,5,0.18 +12154,93.0,10.0,9.0,10.0,10.0,10.0,9.0,181,6.31 +76298,100.0,10.0,9.0,10.0,10.0,10.0,10.0,18,0.64 +52288,97.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.33 +74154,93.0,10.0,9.0,10.0,10.0,10.0,10.0,11,0.39 +38190,91.0,10.0,9.0,10.0,10.0,10.0,10.0,22,0.93 +4624,95.0,10.0,9.0,10.0,10.0,9.0,9.0,22,0.83 +6171,,,,,,,,0, +15481,100.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.2 +35677,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +43083,,,,,,,,0, +22652,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.11 +76843,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.22 +19785,,,,,,,,0, +15972,95.0,10.0,10.0,10.0,10.0,10.0,9.0,101,3.52 +49298,98.0,10.0,9.0,10.0,10.0,10.0,10.0,12,0.44 +21145,95.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.18 +60127,100.0,10.0,9.0,10.0,10.0,9.0,10.0,2,0.07 +8813,81.0,9.0,8.0,9.0,9.0,9.0,8.0,54,1.9 +16597,88.0,9.0,9.0,9.0,10.0,10.0,9.0,83,4.6 +7284,95.0,10.0,10.0,10.0,10.0,10.0,9.0,26,1.04 +52126,,,,,,,,0, +8903,96.0,10.0,10.0,10.0,10.0,9.0,10.0,12,0.48 +41255,90.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.22 +27360,60.0,,,,,,,2,0.07 +26426,92.0,9.0,9.0,10.0,10.0,10.0,10.0,19,0.67 +29997,94.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.26 +46933,,,,,,,,0, +33528,96.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.64 +14641,99.0,10.0,10.0,10.0,10.0,9.0,10.0,29,1.02 +21365,95.0,9.0,10.0,10.0,10.0,10.0,9.0,15,0.6 +45778,,,,,,,,0, +24844,96.0,10.0,10.0,10.0,10.0,9.0,9.0,23,0.86 +2158,,,,,,,,0, +22903,97.0,10.0,10.0,9.0,10.0,9.0,9.0,38,1.36 +57252,97.0,10.0,10.0,10.0,10.0,9.0,9.0,6,0.22 +7479,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,0.94 +9166,92.0,10.0,9.0,10.0,9.0,9.0,10.0,175,6.19 +25040,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.18 +64508,,,,,,,,0, +20737,99.0,10.0,10.0,10.0,10.0,10.0,10.0,58,2.05 +62003,99.0,10.0,10.0,10.0,10.0,10.0,10.0,72,2.58 +70111,100.0,10.0,10.0,10.0,10.0,10.0,10.0,67,2.38 +9443,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +42868,100.0,10.0,10.0,10.0,10.0,10.0,10.0,28,1.92 +65272,97.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.21 +49209,99.0,10.0,10.0,10.0,10.0,10.0,10.0,32,1.61 +73164,96.0,10.0,9.0,10.0,10.0,9.0,10.0,34,1.27 +48899,100.0,10.0,10.0,10.0,10.0,10.0,10.0,33,1.23 +23037,88.0,9.0,9.0,9.0,10.0,10.0,9.0,33,1.17 +24627,89.0,9.0,9.0,10.0,10.0,9.0,9.0,26,0.92 +46339,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.18 +70723,98.0,10.0,10.0,10.0,10.0,10.0,10.0,62,2.18 +9028,94.0,9.0,10.0,10.0,10.0,9.0,9.0,27,0.96 +69387,100.0,10.0,10.0,10.0,9.0,10.0,10.0,2,0.09 +61071,94.0,10.0,9.0,10.0,10.0,10.0,9.0,13,0.5 +2235,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.11 +52659,80.0,10.0,9.0,7.0,10.0,8.0,8.0,2,0.07 +21113,100.0,10.0,10.0,10.0,10.0,9.0,10.0,16,0.58 +45547,96.0,10.0,9.0,10.0,10.0,10.0,10.0,113,3.98 +53170,98.0,10.0,10.0,10.0,10.0,10.0,9.0,8,0.3 +2033,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.07 +46434,86.0,9.0,7.0,10.0,10.0,10.0,9.0,10,0.36 +37710,95.0,10.0,10.0,10.0,10.0,10.0,10.0,67,2.72 +39188,,,,,,,,0, +21064,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.28 +45426,99.0,10.0,10.0,9.0,10.0,10.0,10.0,16,0.57 +7205,98.0,10.0,10.0,10.0,10.0,10.0,10.0,17,1.96 +980,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +11404,98.0,10.0,10.0,10.0,10.0,10.0,10.0,73,3.36 +40948,90.0,10.0,10.0,9.0,10.0,9.0,9.0,4,0.14 +24430,96.0,10.0,10.0,9.0,10.0,10.0,10.0,20,5.13 +52401,83.0,8.0,8.0,9.0,9.0,9.0,9.0,40,1.41 +74208,97.0,10.0,9.0,9.0,9.0,9.0,9.0,6,0.21 +56932,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.08 +76096,,,,,,,,0, +71033,84.0,9.0,9.0,9.0,9.0,10.0,8.0,5,0.18 +31073,88.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.18 +42233,,,,,,,,0, +65473,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.29 +77082,,,,,,,,0, +25273,80.0,9.0,9.0,10.0,10.0,7.0,9.0,3,0.11 +1985,96.0,10.0,10.0,10.0,10.0,10.0,9.0,58,3.19 +68154,93.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.4 +37704,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +64420,97.0,9.0,10.0,10.0,10.0,10.0,9.0,23,0.85 +10913,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.18 +57231,100.0,8.0,8.0,8.0,10.0,8.0,10.0,1,0.04 +17372,89.0,9.0,9.0,9.0,9.0,8.0,9.0,20,0.72 +31216,100.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.74 +22462,99.0,10.0,10.0,10.0,10.0,10.0,10.0,114,4.2 +35089,,,,,,,,0, +58191,91.0,9.0,9.0,9.0,9.0,10.0,9.0,28,1.11 +50650,87.0,8.0,9.0,9.0,10.0,9.0,9.0,3,0.11 +23332,93.0,10.0,9.0,10.0,10.0,9.0,9.0,17,0.64 +73143,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.13 +67395,,,,,,,,0, +72740,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,3.0 +17105,96.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.4 +9399,95.0,10.0,10.0,10.0,9.0,9.0,10.0,35,1.29 +16454,98.0,10.0,10.0,10.0,10.0,9.0,10.0,36,1.33 +72217,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.11 +65887,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.11 +63075,78.0,9.0,9.0,9.0,9.0,9.0,8.0,21,0.77 +548,96.0,10.0,10.0,10.0,10.0,10.0,9.0,35,1.26 +44802,94.0,9.0,10.0,9.0,10.0,10.0,10.0,19,0.7 +12954,97.0,10.0,10.0,10.0,10.0,10.0,9.0,21,0.8 +27014,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.57 +43402,94.0,10.0,10.0,9.0,9.0,10.0,10.0,7,0.33 +75926,94.0,10.0,9.0,10.0,10.0,10.0,9.0,20,0.73 +28268,98.0,10.0,10.0,10.0,10.0,9.0,10.0,63,2.26 +13965,77.0,7.0,8.0,8.0,8.0,9.0,7.0,14,0.53 +47193,,,,,,,,0, +55565,,,,,,,,0, +21425,,,,,,,,1,0.42 +17852,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.14 +53799,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +64567,92.0,10.0,7.0,10.0,10.0,9.0,9.0,23,0.83 +65716,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.69 +48703,,,,,,,,1,0.1 +49660,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.17 +23687,,,,,,,,0, +20871,85.0,8.0,8.0,9.0,9.0,8.0,8.0,47,1.68 +76432,98.0,10.0,10.0,10.0,10.0,10.0,10.0,40,1.45 +65173,,,,,,,,0, +33364,89.0,9.0,9.0,10.0,10.0,10.0,9.0,24,0.87 +5968,99.0,10.0,10.0,10.0,10.0,10.0,10.0,121,4.53 +48234,100.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.14 +64653,,,,,,,,0, +55959,86.0,9.0,9.0,9.0,9.0,8.0,9.0,36,1.3 +48846,70.0,8.0,7.0,8.0,8.0,10.0,7.0,4,0.15 +65674,98.0,10.0,10.0,10.0,10.0,10.0,10.0,42,1.56 +32558,88.0,10.0,9.0,10.0,9.0,10.0,9.0,13,0.47 +16228,97.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.31 +16790,93.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.12 +55677,88.0,10.0,8.0,10.0,10.0,9.0,9.0,7,0.25 +56464,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +50632,,,,,,,,1,0.04 +66722,99.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.64 +65180,90.0,10.0,9.0,9.0,10.0,9.0,9.0,4,0.18 +31879,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +19275,93.0,9.0,9.0,9.0,9.0,9.0,9.0,4,0.17 +35491,94.0,10.0,9.0,10.0,10.0,9.0,10.0,34,1.24 +62412,100.0,10.0,10.0,10.0,10.0,10.0,10.0,44,1.6 +67880,,,,,,,,1,0.2 +4610,,,,,,,,0, +43516,97.0,10.0,10.0,10.0,10.0,10.0,9.0,14,0.84 +42185,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +62536,90.0,9.0,9.0,10.0,10.0,9.0,9.0,3,0.13 +16069,73.0,7.0,6.0,9.0,9.0,10.0,7.0,6,0.22 +53935,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +46381,96.0,10.0,9.0,10.0,10.0,9.0,9.0,20,0.73 +48836,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.46 +56021,91.0,9.0,8.0,10.0,10.0,9.0,9.0,7,0.26 +71822,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.15 +76545,90.0,10.0,9.0,10.0,10.0,10.0,9.0,6,0.25 +10919,,,,,,,,0, +53718,98.0,10.0,10.0,10.0,10.0,9.0,10.0,18,1.01 +9357,95.0,10.0,9.0,10.0,10.0,10.0,10.0,22,1.0 +13026,,,,,,,,0, +15431,97.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.54 +30609,95.0,9.0,9.0,10.0,10.0,9.0,9.0,31,1.17 +46340,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.07 +50214,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.18 +16173,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.18 +59045,100.0,10.0,10.0,9.0,10.0,10.0,10.0,8,0.29 +8234,84.0,9.0,9.0,9.0,9.0,10.0,9.0,60,2.21 +31423,,,,,,,,0, +23117,99.0,10.0,10.0,10.0,10.0,10.0,10.0,65,2.36 +31136,97.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.59 +15908,97.0,10.0,10.0,10.0,10.0,10.0,9.0,132,4.75 +28746,93.0,10.0,9.0,9.0,9.0,10.0,9.0,11,0.41 +50421,97.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.59 +64356,99.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.64 +30159,,,,,,,,0, +60120,,,,,,,,0, +22617,98.0,10.0,10.0,9.0,10.0,10.0,10.0,24,0.87 +37944,99.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.54 +4099,97.0,10.0,10.0,10.0,10.0,9.0,10.0,57,2.39 +943,85.0,9.0,8.0,10.0,9.0,9.0,9.0,89,3.21 +64831,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.15 +4651,100.0,10.0,10.0,,10.0,,,2,0.07 +69806,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,0.07 +35564,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.26 +4578,99.0,10.0,9.0,10.0,10.0,9.0,10.0,54,2.04 +19097,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.07 +36919,96.0,10.0,10.0,10.0,10.0,10.0,9.0,34,1.28 +45107,,,,,,,,1,0.13 +53333,86.0,9.0,9.0,10.0,10.0,8.0,9.0,21,0.77 +17319,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +142,87.0,9.0,9.0,9.0,9.0,8.0,9.0,36,1.31 +13798,77.0,9.0,9.0,10.0,10.0,9.0,9.0,7,0.26 +48901,83.0,9.0,9.0,9.0,9.0,9.0,9.0,92,3.38 +63892,83.0,9.0,8.0,10.0,9.0,9.0,9.0,86,3.18 +41039,86.0,9.0,9.0,10.0,10.0,9.0,9.0,58,2.11 +68691,90.0,9.0,9.0,10.0,10.0,10.0,9.0,25,0.93 +50877,98.0,10.0,9.0,10.0,10.0,10.0,10.0,12,0.44 +33493,98.0,10.0,10.0,10.0,10.0,10.0,10.0,24,0.9 +55782,99.0,10.0,10.0,10.0,10.0,10.0,10.0,71,2.66 +3854,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +6948,71.0,8.0,8.0,6.0,7.0,10.0,7.0,14,0.52 +72836,96.0,10.0,10.0,10.0,10.0,10.0,10.0,68,2.48 +22116,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.79 +21417,98.0,10.0,10.0,10.0,10.0,9.0,9.0,17,0.64 +38896,99.0,10.0,10.0,10.0,10.0,10.0,10.0,190,6.88 +60247,95.0,10.0,9.0,10.0,10.0,10.0,10.0,114,4.14 +64764,84.0,9.0,9.0,9.0,8.0,10.0,9.0,37,1.34 +18939,88.0,9.0,9.0,9.0,9.0,10.0,9.0,20,0.73 +74294,84.0,9.0,9.0,9.0,9.0,9.0,9.0,22,0.89 +63208,79.0,8.0,8.0,8.0,7.0,10.0,9.0,27,1.09 +51887,72.0,7.0,9.0,8.0,7.0,9.0,8.0,12,0.44 +61443,78.0,9.0,10.0,8.0,8.0,10.0,9.0,13,0.48 +26875,86.0,9.0,10.0,9.0,9.0,10.0,9.0,56,2.04 +17727,83.0,8.0,9.0,8.0,9.0,10.0,9.0,7,0.25 +36921,86.0,9.0,9.0,9.0,9.0,10.0,9.0,31,1.21 +52527,83.0,9.0,9.0,9.0,8.0,9.0,8.0,39,1.43 +37607,99.0,10.0,10.0,10.0,10.0,9.0,10.0,76,2.76 +742,,,,,,,,0, +33356,93.0,10.0,10.0,10.0,10.0,9.0,9.0,13,0.49 +47199,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.23 +13216,98.0,10.0,10.0,10.0,10.0,10.0,10.0,37,2.74 +22169,98.0,10.0,10.0,10.0,10.0,9.0,10.0,77,2.85 +50094,91.0,9.0,9.0,9.0,10.0,10.0,9.0,43,1.6 +36657,87.0,9.0,10.0,9.0,10.0,9.0,9.0,11,0.42 +49664,98.0,10.0,10.0,10.0,10.0,10.0,9.0,31,1.15 +17620,79.0,9.0,8.0,9.0,8.0,9.0,8.0,34,1.24 +9548,93.0,10.0,10.0,10.0,10.0,9.0,9.0,36,1.32 +13198,97.0,10.0,10.0,9.0,10.0,9.0,10.0,6,0.23 +56480,99.0,10.0,10.0,10.0,10.0,10.0,10.0,124,4.58 +7597,98.0,10.0,9.0,10.0,10.0,9.0,10.0,28,1.04 +14707,89.0,9.0,9.0,9.0,9.0,9.0,9.0,37,1.36 +55501,83.0,9.0,8.0,9.0,9.0,9.0,9.0,70,2.75 +58986,96.0,10.0,9.0,10.0,10.0,10.0,10.0,106,3.98 +67012,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.56 +46044,90.0,9.0,10.0,9.0,10.0,10.0,8.0,2,0.07 +71469,99.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.08 +22996,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.17 +10124,100.0,8.0,4.0,10.0,10.0,10.0,10.0,1,0.04 +7567,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.17 +63876,91.0,9.0,9.0,10.0,9.0,10.0,9.0,25,0.92 +6642,94.0,10.0,9.0,10.0,10.0,10.0,9.0,8,1.26 +42513,96.0,10.0,9.0,9.0,10.0,10.0,10.0,15,0.57 +71998,95.0,10.0,9.0,10.0,10.0,10.0,10.0,69,2.6 +59895,97.0,9.0,9.0,10.0,10.0,10.0,10.0,7,0.26 +26004,,,,,,,,0, +12505,77.0,8.0,8.0,9.0,9.0,8.0,8.0,26,1.0 +25882,99.0,10.0,10.0,10.0,10.0,10.0,10.0,53,1.95 +76731,96.0,10.0,10.0,10.0,9.0,9.0,9.0,96,3.54 +39840,98.0,10.0,10.0,10.0,10.0,10.0,10.0,44,1.61 +37114,99.0,10.0,10.0,10.0,10.0,10.0,10.0,39,1.46 +74040,,,,,,,,0, +63010,,,,,,,,1,0.04 +17292,99.0,10.0,10.0,10.0,10.0,10.0,10.0,44,1.63 +72280,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +23685,,,,,,,,0, +7144,98.0,10.0,9.0,10.0,10.0,10.0,10.0,18,0.67 +19241,,,,,,,,0, +11540,93.0,9.0,10.0,9.0,10.0,10.0,10.0,16,0.62 +73834,99.0,10.0,10.0,10.0,10.0,9.0,10.0,103,3.77 +3137,90.0,10.0,10.0,9.0,9.0,9.0,9.0,12,0.44 +71409,86.0,9.0,9.0,9.0,9.0,9.0,9.0,10,0.38 +602,90.0,10.0,9.0,10.0,10.0,9.0,10.0,4,0.16 +13500,80.0,9.0,8.0,10.0,10.0,9.0,9.0,3,0.17 +39020,98.0,10.0,10.0,10.0,10.0,9.0,9.0,15,0.57 +32827,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.75 +70751,93.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.13 +32057,86.0,9.0,9.0,10.0,9.0,9.0,9.0,61,2.25 +58096,82.0,9.0,8.0,9.0,9.0,9.0,9.0,84,3.1 +71817,100.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.07 +44389,80.0,8.0,7.0,9.0,10.0,9.0,9.0,2,0.12 +13133,93.0,9.0,10.0,9.0,9.0,9.0,9.0,3,0.11 +38911,96.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.26 +14463,80.0,8.0,8.0,9.0,9.0,8.0,8.0,49,1.93 +38940,90.0,9.0,8.0,9.0,10.0,10.0,9.0,7,0.39 +71085,87.0,9.0,7.0,10.0,10.0,9.0,9.0,24,0.95 +13539,88.0,9.0,8.0,9.0,9.0,10.0,9.0,47,1.77 +61910,94.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.5 +52193,93.0,10.0,10.0,10.0,10.0,9.0,9.0,8,0.32 +30474,96.0,10.0,10.0,10.0,10.0,10.0,10.0,118,4.36 +69856,98.0,10.0,10.0,10.0,10.0,10.0,10.0,27,1.1 +41266,,,,,,,,0, +17298,96.0,10.0,10.0,10.0,10.0,10.0,10.0,117,4.33 +73204,,,,,,,,1,0.04 +49925,,,,,,,,0, +46395,96.0,10.0,9.0,10.0,10.0,10.0,10.0,131,4.86 +74513,100.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.11 +16055,,,,,,,,0, +69424,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.19 +67356,91.0,9.0,9.0,9.0,9.0,10.0,9.0,7,0.3 +37526,93.0,9.0,9.0,9.0,9.0,10.0,9.0,11,0.41 +74872,78.0,8.0,8.0,9.0,10.0,8.0,8.0,10,0.37 +4517,,,,,,,,0, +45969,97.0,10.0,10.0,10.0,10.0,9.0,10.0,49,1.82 +44463,87.0,9.0,10.0,10.0,10.0,9.0,10.0,3,0.11 +63858,,,,,,,,0, +28761,96.0,10.0,10.0,10.0,9.0,9.0,10.0,14,0.57 +4406,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.01 +39973,,,,,,,,1,0.04 +53759,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.15 +73675,,,,,,,,0, +54015,96.0,9.0,9.0,9.0,10.0,10.0,9.0,5,0.19 +36037,,,,,,,,0, +55460,96.0,10.0,9.0,10.0,10.0,10.0,10.0,173,6.38 +55464,97.0,10.0,10.0,10.0,10.0,9.0,10.0,29,1.21 +57188,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.13 +28088,84.0,9.0,8.0,9.0,9.0,10.0,8.0,9,0.34 +13074,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.43 +41753,93.0,9.0,9.0,10.0,9.0,9.0,9.0,40,1.53 +40017,73.0,8.0,9.0,10.0,10.0,8.0,8.0,5,0.19 +3805,96.0,9.0,9.0,10.0,10.0,10.0,10.0,10,0.38 +53589,94.0,10.0,9.0,9.0,10.0,10.0,9.0,8,0.3 +61060,99.0,10.0,10.0,10.0,10.0,10.0,10.0,22,0.87 +29488,90.0,9.0,9.0,10.0,10.0,10.0,9.0,43,1.62 +50661,87.0,10.0,9.0,9.0,10.0,9.0,9.0,4,0.15 +38917,92.0,9.0,9.0,10.0,10.0,10.0,10.0,21,0.79 +21990,95.0,10.0,10.0,10.0,10.0,9.0,10.0,56,2.11 +10281,80.0,8.0,10.0,10.0,10.0,8.0,8.0,1,0.04 +37958,80.0,9.0,10.0,8.0,9.0,9.0,9.0,4,0.15 +8843,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.05 +33229,99.0,10.0,9.0,10.0,10.0,10.0,10.0,38,1.47 +7079,93.0,9.0,9.0,10.0,10.0,9.0,10.0,18,0.72 +73714,98.0,10.0,10.0,10.0,10.0,10.0,10.0,83,3.39 +65019,94.0,10.0,9.0,10.0,10.0,9.0,10.0,122,4.55 +68449,97.0,10.0,10.0,10.0,10.0,9.0,10.0,32,1.22 +76182,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.11 +40021,90.0,9.0,9.0,10.0,9.0,9.0,9.0,2,0.07 +29117,,,,,,,,0, +16731,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.21 +31120,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.22 +4925,,,,,,,,0, +31292,98.0,10.0,9.0,10.0,10.0,9.0,10.0,44,1.79 +63008,92.0,9.0,9.0,10.0,10.0,10.0,9.0,20,0.75 +29852,60.0,4.0,6.0,8.0,8.0,6.0,4.0,1,0.37 +45399,93.0,10.0,9.0,10.0,10.0,10.0,10.0,12,0.47 +22839,98.0,10.0,9.0,10.0,10.0,10.0,10.0,9,1.18 +50174,93.0,10.0,8.0,10.0,10.0,9.0,10.0,6,0.23 +72074,91.0,9.0,8.0,10.0,10.0,10.0,9.0,19,0.72 +50577,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +10237,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.75 +44746,86.0,9.0,9.0,10.0,10.0,10.0,9.0,7,0.26 +58183,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.17 +4273,98.0,10.0,10.0,10.0,10.0,9.0,10.0,25,0.94 +61499,95.0,9.0,10.0,9.0,9.0,10.0,10.0,16,1.22 +18508,99.0,10.0,10.0,10.0,10.0,10.0,10.0,39,1.47 +69725,93.0,9.0,10.0,10.0,9.0,10.0,9.0,74,2.79 +32708,98.0,10.0,9.0,10.0,10.0,10.0,10.0,9,0.34 +52366,96.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.19 +47862,93.0,10.0,9.0,10.0,10.0,8.0,9.0,11,0.51 +51714,98.0,10.0,10.0,10.0,10.0,10.0,10.0,95,3.58 +627,97.0,10.0,10.0,10.0,10.0,10.0,10.0,21,0.78 +3306,97.0,10.0,10.0,10.0,10.0,10.0,10.0,90,3.58 +76609,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.22 +34992,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +16763,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,2.15 +44930,95.0,10.0,9.0,10.0,9.0,10.0,10.0,38,1.74 +33037,,,,,,,,1,0.04 +53616,91.0,9.0,10.0,10.0,10.0,10.0,9.0,18,0.7 +30454,40.0,5.0,5.0,7.0,7.0,6.0,4.0,3,0.34 +73115,88.0,9.0,9.0,10.0,10.0,8.0,9.0,33,1.26 +23142,91.0,9.0,9.0,10.0,9.0,10.0,9.0,13,1.7 +47244,91.0,10.0,9.0,9.0,9.0,10.0,9.0,30,1.14 +69954,96.0,10.0,10.0,10.0,10.0,10.0,10.0,62,2.55 +28771,60.0,8.0,4.0,10.0,8.0,8.0,6.0,1,0.04 +25564,90.0,10.0,10.0,9.0,9.0,9.0,10.0,2,0.08 +6080,,,,,,,,0, +31299,90.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +40409,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.55 +66842,87.0,9.0,9.0,9.0,9.0,9.0,9.0,83,3.1 +56405,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +50980,97.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.36 +52481,81.0,9.0,8.0,10.0,9.0,9.0,8.0,128,4.82 +55314,76.0,8.0,8.0,9.0,9.0,9.0,8.0,130,4.86 +53431,83.0,9.0,8.0,9.0,9.0,9.0,9.0,137,5.11 +27831,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.25 +69072,,,,,,,,0, +34153,97.0,10.0,10.0,10.0,10.0,10.0,10.0,23,0.88 +1118,91.0,9.0,9.0,10.0,10.0,10.0,9.0,33,1.24 +39560,,,,,,,,0, +9848,100.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.79 +2364,86.0,9.0,8.0,9.0,9.0,10.0,9.0,22,0.84 +24808,95.0,10.0,9.0,10.0,10.0,10.0,10.0,21,0.8 +33734,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.12 +55771,,,,,,,,1,0.05 +51829,94.0,10.0,9.0,10.0,10.0,9.0,9.0,38,1.44 +26842,97.0,10.0,10.0,10.0,10.0,10.0,9.0,18,0.69 +45804,,,,,,,,0, +41978,93.0,10.0,10.0,10.0,9.0,9.0,9.0,14,0.53 +533,,,,,,,,2,0.13 +61783,74.0,7.0,9.0,9.0,9.0,10.0,8.0,7,0.34 +34575,97.0,10.0,9.0,10.0,10.0,10.0,10.0,58,2.22 +35410,91.0,10.0,10.0,10.0,10.0,9.0,9.0,9,0.35 +24123,100.0,10.0,10.0,10.0,10.0,8.0,10.0,3,0.43 +15455,,,,,,,,0, +67540,95.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.19 +50084,92.0,9.0,8.0,10.0,10.0,10.0,9.0,36,1.66 +7918,,,,,,,,0, +27366,96.0,10.0,10.0,10.0,10.0,9.0,10.0,58,2.19 +51206,85.0,10.0,8.0,9.0,9.0,10.0,9.0,11,0.43 +54701,90.0,9.0,10.0,10.0,10.0,9.0,9.0,2,0.08 +469,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.49 +29792,98.0,10.0,10.0,10.0,10.0,8.0,9.0,8,3.16 +64463,90.0,10.0,8.0,7.0,10.0,9.0,9.0,2,0.18 +48928,,,,,,,,0, +8255,,,,,,,,0, +27542,91.0,10.0,9.0,10.0,9.0,10.0,9.0,165,6.27 +55240,96.0,10.0,9.0,10.0,10.0,9.0,9.0,15,0.64 +56938,,,,,,,,0, +47627,,,,,,,,0, +37001,,,,,,,,0, +37871,99.0,10.0,10.0,10.0,10.0,10.0,10.0,34,1.33 +44550,88.0,9.0,9.0,9.0,9.0,10.0,9.0,19,0.74 +9174,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.47 +60964,100.0,10.0,9.0,10.0,10.0,9.0,10.0,2,0.09 +57552,90.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.11 +7014,92.0,10.0,10.0,10.0,10.0,10.0,9.0,161,6.08 +25064,,,,,,,,0, +50947,100.0,10.0,10.0,10.0,10.0,10.0,10.0,62,2.38 +6197,98.0,10.0,10.0,10.0,10.0,10.0,9.0,34,1.31 +10165,72.0,7.0,8.0,9.0,9.0,8.0,8.0,21,0.8 +68709,98.0,10.0,10.0,10.0,10.0,10.0,10.0,84,3.19 +43840,99.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.53 +20705,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.37 +50489,97.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.29 +13574,93.0,9.0,9.0,10.0,10.0,9.0,10.0,23,0.89 +18226,99.0,10.0,10.0,10.0,10.0,10.0,10.0,71,2.7 +39430,85.0,9.0,9.0,7.0,9.0,9.0,9.0,16,0.85 +57185,100.0,10.0,9.0,10.0,10.0,9.0,9.0,4,0.16 +47140,60.0,4.0,10.0,10.0,10.0,6.0,4.0,1,0.04 +18781,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.78 +71972,95.0,10.0,10.0,10.0,10.0,10.0,9.0,16,0.86 +1425,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.32 +42158,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.08 +44097,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,2.63 +71762,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.65 +47347,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +23486,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +45409,99.0,10.0,10.0,10.0,10.0,9.0,10.0,37,1.47 +23705,,,,,,,,0, +10830,96.0,10.0,10.0,10.0,10.0,9.0,10.0,25,0.98 +1310,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.11 +58206,,,,,,,,0, +65012,81.0,9.0,8.0,9.0,9.0,9.0,8.0,18,0.94 +62984,90.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +15345,100.0,10.0,10.0,,10.0,,,1,0.04 +30409,93.0,10.0,9.0,10.0,10.0,10.0,9.0,74,2.84 +8768,87.0,9.0,8.0,10.0,10.0,9.0,10.0,7,0.27 +64486,82.0,9.0,10.0,8.0,8.0,10.0,9.0,10,0.4 +12520,87.0,9.0,9.0,9.0,10.0,10.0,8.0,7,0.3 +47874,100.0,10.0,9.0,10.0,10.0,10.0,10.0,5,0.21 +14809,67.0,6.0,7.0,6.0,7.0,7.0,7.0,3,0.16 +27965,96.0,10.0,10.0,10.0,10.0,9.0,10.0,19,0.73 +9477,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +65163,98.0,9.0,10.0,10.0,10.0,10.0,10.0,12,0.47 +11435,,,,,,,,0, +37515,,,,,,,,0, +41177,100.0,10.0,10.0,10.0,10.0,10.0,10.0,23,0.89 +47224,84.0,8.0,7.0,9.0,9.0,8.0,8.0,18,0.7 +75271,96.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.83 +64840,,,,,,,,0, +5076,90.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.11 +53875,94.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.29 +19518,92.0,9.0,9.0,10.0,10.0,10.0,9.0,33,1.25 +34416,99.0,10.0,10.0,10.0,10.0,9.0,10.0,99,3.85 +71798,91.0,9.0,9.0,9.0,9.0,10.0,10.0,34,1.31 +64372,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.11 +29286,,,,,,,,0, +69897,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.04 +62890,95.0,10.0,9.0,10.0,10.0,9.0,10.0,93,3.56 +6713,93.0,9.0,9.0,9.0,9.0,10.0,10.0,3,0.12 +32921,,,,,,,,1,0.04 +70374,98.0,10.0,10.0,10.0,10.0,10.0,10.0,71,2.72 +65831,100.0,10.0,8.0,10.0,10.0,10.0,8.0,2,0.08 +25120,92.0,9.0,9.0,10.0,10.0,8.0,9.0,5,0.21 +15608,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.28 +8224,91.0,10.0,9.0,10.0,10.0,9.0,9.0,36,1.41 +47739,98.0,10.0,10.0,10.0,10.0,10.0,9.0,28,2.12 +3055,80.0,6.0,10.0,6.0,10.0,10.0,8.0,1,0.06 +58435,86.0,9.0,8.0,9.0,10.0,10.0,9.0,19,0.92 +57967,88.0,9.0,8.0,9.0,9.0,9.0,9.0,93,3.56 +31269,,,,,,,,0, +45828,84.0,9.0,9.0,9.0,9.0,9.0,9.0,29,1.12 +12175,88.0,9.0,9.0,8.0,9.0,9.0,9.0,29,1.43 +56385,99.0,10.0,10.0,10.0,10.0,10.0,10.0,36,1.58 +69082,98.0,10.0,10.0,10.0,10.0,9.0,10.0,46,1.77 +24223,87.0,7.0,10.0,6.0,6.0,8.0,9.0,3,0.13 +48424,98.0,10.0,10.0,10.0,10.0,9.0,10.0,8,0.31 +71189,96.0,10.0,10.0,10.0,10.0,9.0,10.0,108,6.98 +4560,,,,,,,,0, +12884,91.0,10.0,8.0,10.0,10.0,10.0,9.0,16,0.61 +26084,93.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.13 +41326,98.0,10.0,10.0,10.0,10.0,9.0,10.0,25,1.03 +73470,94.0,10.0,9.0,10.0,10.0,10.0,9.0,14,1.18 +40511,83.0,8.0,10.0,9.0,8.0,9.0,8.0,12,0.48 +63552,80.0,8.0,9.0,9.0,9.0,10.0,8.0,8,0.32 +6040,88.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.25 +11697,98.0,9.0,10.0,9.0,10.0,10.0,9.0,9,0.38 +36804,96.0,10.0,10.0,10.0,10.0,10.0,10.0,104,4.09 +50761,98.0,9.0,9.0,10.0,10.0,10.0,10.0,8,0.43 +70314,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.12 +11204,20.0,2.0,2.0,2.0,2.0,2.0,2.0,1,0.04 +22384,,,,,,,,0, +73230,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.04 +24007,,,,,,,,0, +25466,96.0,10.0,10.0,10.0,10.0,10.0,10.0,39,1.51 +18488,80.0,6.0,8.0,10.0,10.0,10.0,8.0,1,0.07 +34479,,,,,,,,0, +1376,,,,,,,,0, +59694,84.0,9.0,7.0,10.0,10.0,10.0,9.0,131,5.06 +30585,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.13 +6450,97.0,10.0,10.0,10.0,10.0,10.0,10.0,47,2.0 +23280,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.35 +22326,90.0,9.0,9.0,10.0,10.0,10.0,9.0,26,1.03 +54973,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.11 +40190,,,,,,,,0, +57323,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.12 +46127,97.0,10.0,10.0,10.0,10.0,10.0,9.0,37,1.5 +11662,90.0,10.0,9.0,10.0,10.0,10.0,9.0,4,0.75 +60792,94.0,9.0,9.0,10.0,10.0,9.0,9.0,14,0.6 +6932,93.0,10.0,8.0,10.0,10.0,9.0,9.0,45,1.74 +31922,100.0,9.0,10.0,9.0,10.0,8.0,7.0,2,1.15 +27693,96.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.36 +75564,98.0,10.0,10.0,10.0,10.0,10.0,9.0,23,0.92 +60047,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.81 +21703,91.0,9.0,10.0,9.0,10.0,9.0,9.0,22,0.86 +271,93.0,9.0,9.0,10.0,10.0,10.0,9.0,8,0.37 +44055,86.0,9.0,9.0,9.0,10.0,10.0,9.0,44,1.69 +57398,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +35504,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.12 +5095,,,,,,,,0, +37468,96.0,10.0,10.0,10.0,10.0,9.0,9.0,16,0.62 +64035,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +51900,,,,,,,,0, +29460,,,,,,,,0, +27740,96.0,10.0,10.0,10.0,10.0,9.0,9.0,28,1.11 +41851,95.0,10.0,9.0,10.0,10.0,10.0,10.0,28,1.11 +17716,96.0,10.0,10.0,10.0,10.0,10.0,10.0,62,2.44 +7204,97.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.17 +11141,94.0,9.0,9.0,10.0,9.0,10.0,9.0,54,2.13 +12678,,,,,,,,0, +55800,98.0,10.0,10.0,10.0,10.0,10.0,10.0,25,0.98 +37751,80.0,9.0,10.0,8.0,9.0,8.0,8.0,2,0.13 +40352,85.0,9.0,8.0,9.0,9.0,10.0,9.0,49,1.91 +72852,82.0,9.0,8.0,9.0,10.0,10.0,9.0,11,0.46 +69039,77.0,9.0,8.0,9.0,9.0,10.0,9.0,7,0.28 +52283,89.0,9.0,9.0,9.0,9.0,9.0,9.0,49,1.93 +24412,99.0,10.0,10.0,10.0,10.0,10.0,10.0,55,2.14 +46571,,,,,,,,1, +59412,,,,,,,,0, +22694,100.0,10.0,10.0,10.0,10.0,9.0,10.0,17,1.5 +63299,,,,,,,,0, +69805,97.0,10.0,10.0,10.0,10.0,10.0,9.0,32,1.34 +8754,97.0,10.0,10.0,10.0,10.0,10.0,9.0,16,0.67 +4015,86.0,9.0,9.0,9.0,9.0,10.0,9.0,44,1.73 +44897,92.0,10.0,10.0,10.0,10.0,9.0,9.0,78,3.05 +6018,,,,,,,,2,0.11 +2204,,,,,,,,0, +30191,99.0,10.0,10.0,10.0,10.0,10.0,10.0,39,1.72 +3131,100.0,9.0,9.0,10.0,10.0,8.0,9.0,2,0.09 +69749,100.0,10.0,10.0,10.0,10.0,9.0,10.0,21,0.85 +22905,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +4347,86.0,9.0,8.0,9.0,9.0,8.0,9.0,44,1.71 +26049,92.0,9.0,10.0,9.0,10.0,10.0,9.0,44,1.72 +69019,94.0,10.0,9.0,10.0,10.0,10.0,9.0,10,0.42 +6739,89.0,9.0,8.0,10.0,10.0,9.0,9.0,15,0.62 +41592,84.0,9.0,8.0,10.0,10.0,9.0,8.0,5,0.21 +62225,90.0,9.0,10.0,9.0,9.0,10.0,9.0,29,1.14 +60101,99.0,10.0,10.0,10.0,10.0,10.0,10.0,58,2.29 +50735,,,,,,,,0, +48132,,,,,,,,0, +77055,98.0,10.0,10.0,10.0,10.0,9.0,10.0,19,0.75 +24722,95.0,9.0,9.0,10.0,9.0,9.0,9.0,22,9.57 +42573,,,,,,,,0, +47004,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.05 +56153,85.0,10.0,8.0,9.0,9.0,8.0,8.0,11,0.44 +54839,70.0,9.0,6.0,10.0,8.0,9.0,7.0,2,0.61 +23452,100.0,10.0,8.0,10.0,10.0,8.0,10.0,1,0.04 +51759,78.0,9.0,8.0,9.0,8.0,10.0,8.0,12,0.48 +42013,100.0,9.0,9.0,10.0,10.0,9.0,10.0,3,0.12 +68515,96.0,10.0,9.0,10.0,10.0,9.0,9.0,5,0.27 +26176,92.0,10.0,9.0,9.0,10.0,10.0,9.0,105,4.17 +34795,97.0,10.0,10.0,10.0,10.0,10.0,10.0,45,1.77 +72552,93.0,10.0,9.0,10.0,10.0,10.0,9.0,54,2.19 +53193,80.0,10.0,8.0,10.0,10.0,10.0,8.0,3,1.08 +3139,92.0,9.0,9.0,9.0,10.0,10.0,9.0,29,1.15 +13053,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.04 +39721,85.0,9.0,9.0,9.0,9.0,9.0,9.0,8,0.33 +54639,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.2 +26628,100.0,,10.0,,,,,1,0.06 +26708,98.0,10.0,9.0,10.0,10.0,10.0,10.0,22,0.87 +45155,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.24 +22232,96.0,10.0,9.0,10.0,10.0,10.0,9.0,88,3.53 +75633,93.0,9.0,9.0,10.0,10.0,10.0,9.0,14,0.58 +40161,97.0,10.0,10.0,10.0,10.0,10.0,10.0,45,1.81 +39734,98.0,10.0,10.0,10.0,10.0,10.0,10.0,48,2.36 +12930,,,,,,,,1,0.53 +75022,97.0,10.0,10.0,10.0,10.0,10.0,10.0,71,2.9 +34819,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.3 +44954,100.0,9.0,9.0,10.0,10.0,9.0,10.0,4,0.18 +15272,87.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.12 +68557,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.28 +53356,,,,,,,,0, +15819,96.0,10.0,9.0,10.0,10.0,10.0,10.0,39,1.56 +3535,85.0,9.0,8.0,9.0,9.0,9.0,9.0,43,1.71 +74125,100.0,10.0,10.0,10.0,10.0,10.0,10.0,42,1.86 +56070,93.0,10.0,10.0,10.0,10.0,10.0,9.0,48,3.17 +13301,99.0,10.0,9.0,10.0,10.0,9.0,10.0,16,0.68 +65786,97.0,10.0,10.0,10.0,10.0,10.0,10.0,23,1.13 +65502,95.0,9.0,10.0,10.0,10.0,10.0,9.0,11,0.47 +71511,99.0,10.0,10.0,10.0,10.0,10.0,10.0,21,1.5 +17064,,,,,,,,0, +42198,,,,,,,,0, +10490,96.0,10.0,10.0,10.0,10.0,9.0,10.0,41,1.74 +2414,,,,,,,,0, +16715,89.0,9.0,10.0,9.0,9.0,10.0,9.0,30,1.19 +65984,96.0,10.0,8.0,10.0,10.0,10.0,9.0,30,1.27 +36599,100.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.13 +57549,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +49159,96.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.31 +54066,97.0,10.0,10.0,10.0,10.0,10.0,9.0,21,0.93 +27901,,,,,,,,2,0.08 +61635,93.0,10.0,9.0,10.0,10.0,9.0,9.0,151,6.03 +28624,90.0,9.0,9.0,9.0,10.0,10.0,9.0,102,4.0 +64994,96.0,10.0,10.0,10.0,10.0,9.0,10.0,32,1.46 +35236,60.0,6.0,4.0,10.0,10.0,8.0,6.0,2,0.08 +65865,,,,,,,,0, +17709,95.0,10.0,10.0,9.0,9.0,10.0,10.0,11,0.53 +42635,100.0,9.0,10.0,10.0,10.0,10.0,10.0,4,0.16 +31771,90.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.19 +11857,100.0,10.0,10.0,10.0,8.0,10.0,10.0,1,0.07 +14570,,,,,,,,0, +65732,85.0,9.0,9.0,10.0,10.0,9.0,9.0,107,4.31 +53422,88.0,9.0,9.0,10.0,10.0,10.0,9.0,111,4.45 +14102,99.0,10.0,10.0,10.0,10.0,10.0,10.0,30,1.35 +70763,,,,,,,,0, +9119,100.0,10.0,9.0,10.0,10.0,10.0,10.0,8,0.31 +31457,,,,,,,,0, +76864,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.77 +66360,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.76 +63007,,,,,,,,1,0.04 +3973,,,,,,,,0, +781,100.0,10.0,10.0,10.0,10.0,9.0,9.0,10,0.54 +5925,,,,,,,,0, +22787,,,,,,,,0, +29989,93.0,10.0,10.0,10.0,10.0,9.0,10.0,18,1.93 +664,93.0,10.0,10.0,10.0,10.0,10.0,9.0,20,0.83 +28732,98.0,10.0,10.0,10.0,10.0,10.0,10.0,22,0.88 +55691,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,0.08 +44384,,,,,,,,0, +38178,98.0,10.0,9.0,10.0,10.0,10.0,9.0,10,0.42 +25302,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +49472,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.25 +59410,93.0,10.0,9.0,10.0,10.0,10.0,9.0,4,0.16 +27955,99.0,10.0,10.0,10.0,10.0,10.0,10.0,63,3.07 +42871,67.0,7.0,7.0,10.0,9.0,10.0,8.0,3,0.12 +65768,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.22 +52391,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.75 +42877,97.0,10.0,10.0,10.0,10.0,9.0,9.0,41,1.74 +2615,,,,,,,,0, +66598,,,,,,,,0, +71810,93.0,9.0,9.0,10.0,10.0,9.0,9.0,3,0.12 +18947,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.11 +10075,89.0,8.0,10.0,9.0,10.0,8.0,9.0,9,0.36 +50637,,,,,,,,0, +76273,77.0,9.0,8.0,9.0,9.0,9.0,9.0,7,0.3 +29101,98.0,10.0,10.0,9.0,9.0,10.0,9.0,10,0.4 +16747,,,,,,,,0, +34127,100.0,9.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +506,92.0,10.0,9.0,10.0,10.0,9.0,9.0,103,4.13 +67811,86.0,9.0,8.0,9.0,9.0,9.0,9.0,7,0.32 +9176,,,,,,,,0, +76214,,,,,,,,0, +46569,96.0,10.0,10.0,10.0,10.0,10.0,10.0,46,1.84 +63518,73.0,7.0,10.0,10.0,8.0,7.0,7.0,3,0.12 +35820,91.0,10.0,9.0,10.0,9.0,9.0,10.0,9,0.42 +63265,98.0,10.0,10.0,10.0,10.0,10.0,10.0,33,1.31 +50660,86.0,9.0,9.0,9.0,10.0,10.0,9.0,17,0.69 +14715,94.0,9.0,9.0,10.0,10.0,9.0,9.0,25,1.01 +31072,,,,,,,,0, +58966,79.0,8.0,9.0,9.0,9.0,10.0,8.0,18,0.74 +71054,,,,,,,,0, +76761,98.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.44 +29417,97.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.26 +26862,96.0,10.0,10.0,9.0,10.0,9.0,9.0,47,1.9 +6504,92.0,10.0,9.0,10.0,10.0,10.0,10.0,52,2.09 +21070,,,,,,,,0, +23753,,,,,,,,0, +41702,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.41 +17619,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +18957,,,,,,,,0, +61317,100.0,10.0,10.0,10.0,10.0,4.0,8.0,3,0.12 +15160,,,,,,,,0, +19348,,,,,,,,0, +45020,,,,,,,,0, +31125,,,,,,,,0, +13586,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.13 +63696,83.0,9.0,8.0,9.0,9.0,8.0,8.0,28,1.19 +54646,97.0,10.0,10.0,10.0,10.0,10.0,9.0,23,0.93 +50754,,,,,,,,1,0.05 +16856,100.0,10.0,10.0,9.0,10.0,10.0,10.0,2,0.08 +75857,,,,,,,,0, +76643,,,,,,,,0, +71170,96.0,10.0,10.0,10.0,10.0,9.0,10.0,149,6.05 +11961,87.0,10.0,9.0,10.0,10.0,10.0,9.0,6,0.24 +10779,97.0,10.0,10.0,10.0,10.0,10.0,9.0,64,2.61 +42467,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.04 +22924,100.0,10.0,10.0,10.0,10.0,10.0,10.0,88,3.62 +74028,95.0,9.0,9.0,10.0,10.0,8.0,9.0,4,0.2 +59931,90.0,10.0,9.0,10.0,10.0,10.0,9.0,19,0.98 +1837,96.0,10.0,10.0,10.0,10.0,9.0,10.0,127,5.13 +27224,60.0,8.0,4.0,10.0,10.0,8.0,6.0,1,0.04 +19463,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.1 +42387,80.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.12 +21974,97.0,10.0,9.0,10.0,10.0,10.0,10.0,20,0.85 +51334,90.0,10.0,10.0,9.0,9.0,10.0,8.0,5,0.2 +28207,,,,,,,,0, +15986,80.0,9.0,6.0,8.0,8.0,9.0,9.0,3,0.13 +2997,93.0,10.0,8.0,9.0,9.0,10.0,9.0,6,0.25 +4093,90.0,9.0,10.0,8.0,10.0,10.0,9.0,14,0.6 +2937,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.23 +983,93.0,10.0,9.0,10.0,10.0,10.0,10.0,49,8.35 +15371,92.0,9.0,9.0,9.0,9.0,9.0,9.0,160,6.76 +29255,60.0,6.0,6.0,10.0,10.0,10.0,8.0,1,0.04 +21850,95.0,10.0,10.0,10.0,10.0,9.0,10.0,26,1.06 +28509,88.0,10.0,8.0,10.0,10.0,9.0,8.0,5,0.21 +339,100.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.01 +50584,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.35 +56833,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.2 +56607,,,,,,,,0, +50049,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +48200,88.0,9.0,9.0,8.0,9.0,10.0,9.0,10,0.59 +50908,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.45 +71840,99.0,10.0,10.0,10.0,10.0,10.0,10.0,49,2.37 +32241,83.0,9.0,8.0,9.0,9.0,9.0,9.0,71,2.86 +42681,,,,,,,,0, +44905,60.0,,4.0,,,,,1,0.04 +66148,91.0,9.0,9.0,9.0,10.0,10.0,9.0,8,0.52 +47268,98.0,10.0,10.0,10.0,10.0,9.0,10.0,27,1.13 +15287,98.0,10.0,10.0,10.0,10.0,9.0,10.0,39,1.65 +64210,95.0,10.0,10.0,10.0,10.0,9.0,9.0,39,1.64 +21168,100.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.17 +50773,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.21 +45359,90.0,9.0,10.0,9.0,9.0,9.0,9.0,14,0.58 +42597,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +14267,84.0,9.0,8.0,9.0,9.0,9.0,9.0,81,3.47 +10159,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +34186,97.0,10.0,10.0,10.0,10.0,9.0,10.0,8,0.42 +59680,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.11 +70744,,,,,,,,0, +45696,95.0,10.0,9.0,10.0,10.0,10.0,9.0,4,0.23 +47427,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.33 +65325,,,,,,,,1,1.0 +246,91.0,10.0,10.0,8.0,10.0,9.0,9.0,18,0.97 +26256,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.43 +23972,100.0,10.0,10.0,9.0,10.0,10.0,10.0,6,0.27 +66765,89.0,9.0,9.0,10.0,10.0,10.0,9.0,17,0.72 +30839,70.0,9.0,7.0,10.0,9.0,8.0,10.0,2,0.08 +31773,90.0,10.0,10.0,10.0,10.0,8.0,8.0,2,0.09 +46599,97.0,10.0,10.0,10.0,10.0,10.0,10.0,35,1.49 +41157,,,,,,,,0, +20410,93.0,10.0,10.0,9.0,9.0,10.0,9.0,12,0.54 +67990,98.0,10.0,10.0,10.0,10.0,9.0,9.0,33,1.36 +49499,85.0,8.0,9.0,9.0,9.0,10.0,9.0,8,0.33 +50934,100.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.38 +15653,91.0,10.0,9.0,10.0,10.0,10.0,9.0,17,0.71 +55180,,,,,,,,0, +16670,87.0,9.0,9.0,9.0,10.0,9.0,9.0,25,1.02 +56626,93.0,9.0,10.0,10.0,10.0,10.0,10.0,12,0.59 +76128,93.0,10.0,9.0,9.0,10.0,9.0,9.0,12,0.51 +24682,93.0,10.0,10.0,9.0,10.0,10.0,9.0,23,0.98 +27494,20.0,2.0,2.0,2.0,2.0,2.0,2.0,1,0.75 +58117,98.0,10.0,10.0,10.0,10.0,10.0,9.0,8,0.36 +27466,96.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.29 +45521,92.0,10.0,10.0,10.0,10.0,10.0,10.0,33,1.36 +7833,80.0,7.0,8.0,9.0,9.0,9.0,8.0,3,0.12 +13766,100.0,8.0,10.0,8.0,10.0,10.0,10.0,1,0.37 +16940,,,,,,,,0, +38369,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.75 +52948,92.0,8.0,8.0,10.0,9.0,9.0,9.0,9,0.37 +34934,80.0,9.0,7.0,8.0,8.0,9.0,8.0,76,3.26 +51179,91.0,10.0,9.0,10.0,9.0,8.0,9.0,27,1.19 +2646,90.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.19 +65289,97.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.55 +53287,93.0,9.0,9.0,10.0,10.0,9.0,9.0,4,0.17 +36207,63.0,7.0,6.0,7.0,7.0,9.0,7.0,8,0.37 +2680,80.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.04 +1300,95.0,10.0,9.0,10.0,9.0,10.0,9.0,19,0.81 +20282,99.0,10.0,10.0,10.0,10.0,9.0,10.0,42,1.79 +2285,99.0,10.0,10.0,10.0,10.0,10.0,10.0,71,2.92 +75403,,,,,,,,0, +64082,95.0,10.0,10.0,10.0,9.0,10.0,10.0,4,0.17 +14414,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.41 +65531,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.58 +23658,80.0,9.0,7.0,9.0,9.0,9.0,8.0,30,1.23 +20077,84.0,8.0,8.0,10.0,10.0,10.0,8.0,5,0.29 +10893,100.0,10.0,10.0,10.0,10.0,10.0,10.0,47,2.12 +60456,92.0,10.0,8.0,9.0,10.0,10.0,9.0,5,0.21 +38157,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.25 +21492,99.0,10.0,10.0,10.0,10.0,10.0,10.0,33,1.48 +2817,,,,,,,,0, +42786,90.0,10.0,8.0,10.0,10.0,8.0,9.0,2,0.34 +8454,77.0,8.0,7.0,9.0,9.0,8.0,7.0,17,0.8 +43492,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.13 +4351,96.0,10.0,9.0,10.0,10.0,10.0,9.0,26,1.11 +21371,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.31 +59001,90.0,9.0,8.0,10.0,9.0,9.0,9.0,14,0.8 +18332,,,,,,,,0, +50399,,,,,,,,1,0.04 +74524,86.0,9.0,8.0,9.0,9.0,10.0,9.0,14,0.59 +22293,93.0,9.0,9.0,9.0,9.0,9.0,9.0,4,0.16 +60591,91.0,10.0,9.0,9.0,10.0,10.0,9.0,29,1.3 +63912,,,,,,,,0, +25133,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.54 +23625,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.43 +45879,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.09 +33691,,,,,,,,0, +54460,95.0,9.0,9.0,10.0,10.0,9.0,10.0,5,0.21 +34179,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.13 +11886,93.0,9.0,10.0,9.0,9.0,10.0,9.0,15,0.65 +46369,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.66 +29947,100.0,10.0,10.0,10.0,10.0,10.0,10.0,36,1.6 +16146,98.0,10.0,10.0,10.0,10.0,9.0,10.0,19,0.98 +68310,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.43 +10950,99.0,10.0,10.0,10.0,10.0,10.0,10.0,78,3.37 +1658,80.0,8.0,8.0,10.0,10.0,10.0,8.0,2,0.08 +18607,60.0,5.0,6.0,6.0,6.0,9.0,7.0,3,0.12 +53922,98.0,10.0,10.0,10.0,10.0,10.0,10.0,101,4.16 +2467,93.0,9.0,9.0,9.0,9.0,10.0,9.0,6,0.34 +20660,98.0,10.0,10.0,10.0,10.0,9.0,10.0,30,1.27 +63276,,,,,,,,0, +7301,95.0,10.0,10.0,10.0,10.0,10.0,9.0,51,2.36 +76709,85.0,10.0,8.0,9.0,9.0,10.0,9.0,5,0.2 +11415,,,,,,,,1,0.04 +10234,99.0,10.0,10.0,10.0,10.0,10.0,10.0,60,2.47 +50582,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.04 +2678,100.0,9.0,9.0,9.0,9.0,9.0,9.0,3,0.16 +72622,93.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.15 +21392,98.0,10.0,10.0,10.0,10.0,9.0,10.0,21,0.94 +24891,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.13 +73251,100.0,10.0,10.0,10.0,9.0,10.0,9.0,3,0.41 +17101,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +50687,94.0,10.0,9.0,10.0,10.0,9.0,10.0,18,1.47 +72667,93.0,10.0,10.0,10.0,9.0,9.0,10.0,6,0.25 +55075,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.29 +73962,,,,,,,,0, +41862,92.0,9.0,10.0,9.0,10.0,10.0,9.0,43,1.82 +64747,,,,,,,,1,0.08 +24000,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.08 +70467,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +63168,80.0,8.0,6.0,10.0,8.0,8.0,8.0,1,0.06 +19022,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,0.04 +56218,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.34 +59933,,,,,,,,0, +72083,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.31 +64226,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +54759,,,,,,,,0, +76651,94.0,10.0,9.0,10.0,10.0,10.0,9.0,16,0.9 +59251,100.0,9.0,10.0,10.0,10.0,10.0,9.0,3,0.12 +75527,88.0,9.0,10.0,9.0,9.0,9.0,9.0,34,1.4 +20010,,,,,,,,0, +2644,60.0,6.0,4.0,8.0,10.0,10.0,6.0,1,0.04 +15773,93.0,10.0,9.0,9.0,9.0,9.0,9.0,9,0.38 +41332,,,,,,,,0, +27981,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.18 +2419,,,,,,,,0, +41055,80.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.07 +6251,100.0,10.0,8.0,10.0,10.0,,,1,0.06 +12071,,,,,,,,0, +28450,85.0,8.0,10.0,9.0,9.0,10.0,8.0,4,0.17 +8237,,,,,,,,0, +34279,93.0,9.0,7.0,9.0,9.0,9.0,9.0,3,0.13 +20241,,,,,,,,1,0.04 +37171,95.0,10.0,9.0,10.0,10.0,9.0,9.0,23,0.98 +23892,94.0,9.0,10.0,9.0,9.0,10.0,8.0,10,0.43 +940,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.26 +70080,100.0,10.0,10.0,10.0,10.0,8.0,10.0,5,0.21 +18612,70.0,9.0,5.0,7.0,8.0,10.0,8.0,2,0.09 +9450,97.0,10.0,10.0,10.0,10.0,10.0,10.0,50,2.36 +38201,80.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.05 +2561,,,,,,,,0, +11883,99.0,10.0,10.0,10.0,10.0,10.0,9.0,17,0.72 +38156,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.21 +60945,99.0,10.0,10.0,10.0,10.0,10.0,10.0,195,8.3 +17397,80.0,8.0,8.0,8.0,6.0,8.0,8.0,1,0.04 +44942,93.0,10.0,9.0,10.0,10.0,9.0,9.0,19,0.88 +5235,83.0,8.0,9.0,9.0,10.0,9.0,9.0,12,0.5 +47181,93.0,9.0,9.0,10.0,10.0,9.0,10.0,3,0.13 +56241,97.0,10.0,9.0,10.0,10.0,10.0,10.0,18,0.77 +22722,97.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.13 +49388,98.0,10.0,10.0,10.0,10.0,10.0,10.0,43,1.8 +67270,,,,,,,,0, +70074,90.0,10.0,9.0,10.0,10.0,10.0,9.0,11,0.48 +60266,,,,,,,,0, +6023,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +41467,98.0,10.0,10.0,10.0,10.0,9.0,10.0,22,1.25 +55415,60.0,6.0,4.0,2.0,6.0,8.0,4.0,1,0.04 +37680,98.0,10.0,10.0,10.0,10.0,10.0,10.0,33,1.39 +39239,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.08 +33715,,,,,,,,0, +16854,,,,,,,,0, +47131,,,,,,,,0, +36606,100.0,10.0,10.0,10.0,10.0,10.0,9.0,16,0.85 +1293,98.0,9.0,10.0,9.0,10.0,10.0,10.0,11,0.47 +76773,91.0,10.0,9.0,10.0,10.0,10.0,9.0,25,1.08 +1785,,,,,,,,0, +56557,100.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.35 +61386,83.0,8.0,9.0,9.0,9.0,8.0,8.0,13,0.55 +5860,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +54181,92.0,10.0,9.0,10.0,10.0,9.0,9.0,12,1.04 +24203,87.0,9.0,9.0,10.0,9.0,8.0,9.0,13,0.54 +63886,,,,,,,,0, +63712,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.2 +63679,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.25 +63971,98.0,10.0,10.0,10.0,10.0,10.0,10.0,48,2.01 +59834,86.0,8.0,8.0,10.0,9.0,10.0,8.0,7,0.29 +47814,88.0,9.0,8.0,10.0,10.0,9.0,9.0,5,0.26 +32861,,,,,,,,0, +9518,,,,,,,,0, +48510,94.0,10.0,9.0,10.0,10.0,9.0,10.0,7,0.3 +2220,100.0,9.0,10.0,9.0,9.0,10.0,9.0,3,0.13 +49975,,,,,,,,0, +27590,,,,,,,,0, +67236,97.0,10.0,10.0,10.0,10.0,9.0,9.0,105,5.46 +2542,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +15791,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.36 +39976,87.0,9.0,8.0,10.0,9.0,10.0,9.0,9,0.38 +59683,99.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.69 +10401,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.34 +42722,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.17 +19364,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +16511,96.0,10.0,10.0,10.0,10.0,10.0,9.0,49,2.08 +22277,98.0,9.0,9.0,9.0,9.0,10.0,10.0,9,0.45 +59266,95.0,10.0,10.0,10.0,10.0,10.0,9.0,53,2.25 +40685,96.0,10.0,10.0,10.0,10.0,9.0,10.0,31,1.31 +73971,89.0,9.0,9.0,10.0,10.0,10.0,9.0,17,0.75 +11137,,,,,,,,0, +44553,92.0,10.0,8.0,10.0,10.0,9.0,9.0,5,0.21 +13467,90.0,9.0,9.0,9.0,9.0,9.0,9.0,12,0.52 +57410,100.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.23 +53665,98.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.55 +36549,80.0,9.0,9.0,10.0,10.0,9.0,9.0,5,0.26 +67596,,,,,,,,0, +2517,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.13 +60660,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.22 +45131,,,,,,,,1,0.5 +13436,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.13 +19818,98.0,10.0,10.0,10.0,10.0,10.0,10.0,61,2.79 +60109,95.0,10.0,10.0,10.0,10.0,10.0,10.0,49,2.12 +36894,,,,,,,,0, +76570,,,,,,,,0, +14844,98.0,10.0,10.0,10.0,10.0,9.0,10.0,30,1.46 +42949,,,,,,,,0, +51973,93.0,9.0,10.0,10.0,10.0,9.0,9.0,36,1.53 +65769,,,,,,,,0, +2377,,,,,,,,0, +76756,97.0,10.0,10.0,9.0,10.0,10.0,10.0,7,0.3 +32587,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.13 +41581,,,,,,,,0, +40405,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +73249,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.09 +43870,92.0,9.0,9.0,10.0,10.0,10.0,10.0,5,0.21 +18989,94.0,9.0,9.0,10.0,10.0,10.0,9.0,67,2.8 +65005,78.0,9.0,8.0,10.0,9.0,9.0,8.0,10,0.42 +63325,,,,,,,,0, +75236,85.0,9.0,9.0,9.0,9.0,10.0,8.0,15,0.8 +8184,86.0,9.0,8.0,9.0,9.0,10.0,8.0,7,0.33 +7535,96.0,9.0,9.0,10.0,10.0,9.0,9.0,12,0.68 +59328,80.0,8.0,6.0,6.0,10.0,10.0,6.0,1,0.41 +76338,93.0,10.0,10.0,10.0,10.0,10.0,9.0,66,2.81 +76997,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.01 +42030,,,,,,,,0, +5156,95.0,10.0,9.0,10.0,10.0,9.0,9.0,44,1.87 +39844,,,,,,,,0, +55251,96.0,10.0,9.0,10.0,10.0,9.0,10.0,47,2.32 +18396,98.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.84 +67710,,,,,,,,0, +40230,,,,,,,,0, +62861,93.0,9.0,10.0,10.0,9.0,10.0,9.0,37,1.56 +75560,86.0,9.0,9.0,9.0,9.0,10.0,9.0,28,1.18 +56114,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.09 +7259,97.0,10.0,10.0,10.0,10.0,9.0,10.0,19,0.98 +44153,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.47 +8170,93.0,10.0,10.0,9.0,9.0,8.0,9.0,3,0.13 +29059,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.17 +56740,88.0,9.0,9.0,9.0,9.0,10.0,9.0,15,0.66 +45415,,,,,,,,0, +72518,85.0,8.0,8.0,9.0,10.0,9.0,9.0,11,0.49 +59195,,,,,,,,0, +1251,97.0,10.0,10.0,10.0,10.0,10.0,10.0,67,2.93 +56969,96.0,10.0,9.0,10.0,10.0,10.0,9.0,46,2.24 +69626,96.0,10.0,10.0,10.0,10.0,9.0,9.0,18,0.91 +40623,96.0,10.0,9.0,10.0,10.0,10.0,10.0,17,0.73 +46397,92.0,9.0,9.0,10.0,10.0,10.0,9.0,31,1.41 +48396,73.0,8.0,8.0,9.0,9.0,10.0,8.0,6,0.28 +45217,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.09 +34196,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.17 +36397,,,,,,,,0, +52706,98.0,10.0,9.0,10.0,10.0,10.0,10.0,45,1.93 +26134,80.0,8.0,8.0,10.0,10.0,10.0,8.0,2,0.4 +52817,91.0,9.0,9.0,9.0,10.0,10.0,9.0,109,4.92 +73805,,,,,,,,0, +21931,92.0,10.0,9.0,9.0,10.0,10.0,9.0,103,4.48 +66001,95.0,9.0,10.0,10.0,10.0,9.0,9.0,31,1.58 +11808,100.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.09 +40802,,,,,,,,0, +7220,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.09 +3778,,,,,,,,0, +17984,99.0,10.0,10.0,10.0,10.0,10.0,10.0,77,3.32 +4959,100.0,10.0,10.0,10.0,10.0,10.0,10.0,47,2.14 +41964,84.0,9.0,8.0,9.0,9.0,9.0,9.0,17,0.82 +31811,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +12923,98.0,10.0,10.0,10.0,10.0,10.0,10.0,106,4.63 +26918,99.0,10.0,10.0,10.0,10.0,10.0,10.0,96,4.2 +41913,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.3 +51018,,,,,,,,0, +46745,85.0,9.0,8.0,10.0,10.0,10.0,9.0,51,2.23 +74464,88.0,9.0,9.0,9.0,9.0,9.0,9.0,16,0.68 +918,,,,,,,,0, +28175,93.0,9.0,9.0,10.0,10.0,10.0,10.0,8,0.35 +28568,90.0,8.0,9.0,10.0,10.0,9.0,9.0,21,0.95 +63164,78.0,9.0,8.0,10.0,10.0,8.0,8.0,9,0.43 +73919,80.0,8.0,9.0,9.0,9.0,8.0,8.0,19,0.82 +49050,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.14 +14959,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.52 +64518,100.0,10.0,10.0,9.0,8.0,9.0,9.0,6,0.3 +34612,94.0,10.0,9.0,10.0,10.0,9.0,10.0,32,1.39 +2241,100.0,10.0,10.0,10.0,10.0,10.0,8.0,2,0.41 +38647,87.0,10.0,8.0,10.0,10.0,10.0,10.0,6,0.28 +40034,98.0,10.0,10.0,10.0,10.0,9.0,10.0,34,2.41 +33278,60.0,7.0,5.0,6.0,8.0,10.0,7.0,2,0.09 +66955,,,,,,,,4,0.23 +70081,95.0,10.0,8.0,10.0,10.0,10.0,9.0,25,1.34 +22135,,,,,,,,0, +71985,,,,,,,,0, +69049,94.0,10.0,9.0,10.0,10.0,10.0,9.0,53,2.35 +76343,,,,,,,,0, +55994,,,,,,,,0, +47938,93.0,9.0,9.0,10.0,9.0,10.0,9.0,18,0.9 +23129,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,0.14 +36056,95.0,10.0,9.0,10.0,10.0,10.0,10.0,47,2.07 +17499,20.0,2.0,2.0,6.0,2.0,2.0,2.0,1,0.04 +31149,100.0,10.0,10.0,10.0,10.0,10.0,10.0,24,1.25 +26744,98.0,10.0,9.0,10.0,10.0,9.0,10.0,17,0.75 +66022,97.0,10.0,10.0,10.0,10.0,10.0,10.0,24,4.14 +21851,90.0,10.0,7.0,10.0,10.0,10.0,10.0,3,0.14 +52056,,,,,,,,0, +31282,100.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.95 +3547,99.0,10.0,10.0,10.0,10.0,10.0,10.0,32,1.76 +71806,,,,,,,,0, +56237,,,,,,,,0, +5232,91.0,9.0,10.0,10.0,10.0,9.0,9.0,14,0.63 +14036,91.0,9.0,9.0,9.0,9.0,9.0,9.0,29,1.3 +14003,97.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.61 +64461,98.0,10.0,10.0,10.0,10.0,9.0,10.0,40,1.77 +29145,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.59 +22012,94.0,10.0,9.0,10.0,10.0,10.0,10.0,10,0.48 +25286,80.0,10.0,8.0,10.0,10.0,8.0,10.0,1,0.73 +18890,95.0,10.0,9.0,10.0,10.0,10.0,10.0,130,5.8 +71182,95.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.34 +73697,94.0,9.0,10.0,10.0,10.0,9.0,9.0,35,1.54 +46868,80.0,10.0,6.0,10.0,10.0,10.0,8.0,1,0.05 +41993,86.0,9.0,8.0,10.0,9.0,9.0,9.0,37,1.73 +38646,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.35 +36532,94.0,9.0,10.0,10.0,10.0,9.0,9.0,17,0.94 +13526,95.0,10.0,10.0,10.0,10.0,10.0,10.0,97,4.22 +20706,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.41 +72431,98.0,10.0,10.0,10.0,10.0,10.0,9.0,22,0.98 +7221,,,,,,,,0, +22319,100.0,10.0,10.0,10.0,10.0,9.0,9.0,7,0.32 +35720,95.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.56 +61010,,,,,,,,0, +24950,96.0,10.0,10.0,10.0,10.0,10.0,10.0,114,4.94 +61870,,,,,,,,0, +57668,99.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.29 +67571,100.0,10.0,10.0,10.0,10.0,10.0,10.0,36,1.57 +16247,96.0,10.0,9.0,10.0,10.0,9.0,10.0,11,0.48 +71093,97.0,10.0,10.0,10.0,10.0,9.0,10.0,23,1.25 +28142,100.0,10.0,10.0,10.0,10.0,9.0,10.0,18,0.89 +31834,,,,,,,,0, +65690,93.0,10.0,10.0,10.0,10.0,10.0,10.0,46,2.0 +1098,77.0,8.0,9.0,9.0,8.0,9.0,8.0,6,0.28 +66967,100.0,10.0,10.0,10.0,10.0,10.0,10.0,74,3.38 +60322,,,,,,,,0, +21186,100.0,10.0,10.0,10.0,10.0,10.0,10.0,21,0.97 +18121,,,,,,,,0, +46540,88.0,9.0,10.0,10.0,10.0,9.0,9.0,8,0.35 +68087,96.0,10.0,10.0,10.0,10.0,10.0,10.0,75,3.26 +33596,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.41 +16373,99.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.89 +48467,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.16 +22111,83.0,9.0,8.0,9.0,10.0,10.0,9.0,8,0.49 +67088,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +35708,97.0,10.0,10.0,10.0,10.0,10.0,10.0,20,0.95 +37027,98.0,10.0,10.0,10.0,10.0,9.0,9.0,11,0.55 +24564,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,1.17 +31141,94.0,10.0,9.0,10.0,10.0,9.0,9.0,82,3.58 +39478,91.0,10.0,9.0,10.0,10.0,10.0,9.0,61,2.67 +41030,94.0,10.0,10.0,10.0,9.0,10.0,9.0,7,0.33 +42841,,,,,,,,0, +65424,94.0,9.0,10.0,10.0,10.0,10.0,9.0,29,1.31 +10395,,,,,,,,0, +1384,86.0,9.0,8.0,10.0,9.0,9.0,9.0,27,1.19 +14635,95.0,10.0,10.0,10.0,10.0,9.0,9.0,39,1.72 +15014,99.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.64 +844,60.0,6.0,10.0,6.0,8.0,8.0,6.0,1,0.06 +21147,95.0,10.0,10.0,10.0,10.0,8.0,10.0,4,0.19 +76976,60.0,10.0,10.0,4.0,4.0,8.0,8.0,1,0.14 +14315,78.0,8.0,7.0,8.0,8.0,9.0,8.0,8,0.37 +8320,99.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.74 +25267,93.0,10.0,9.0,10.0,10.0,9.0,9.0,30,2.93 +54047,98.0,10.0,10.0,10.0,10.0,9.0,10.0,38,1.76 +73877,100.0,10.0,8.0,8.0,10.0,10.0,10.0,1,0.04 +32507,40.0,3.0,7.0,8.0,6.0,8.0,8.0,3,0.2 +36293,99.0,10.0,10.0,9.0,10.0,9.0,9.0,18,1.04 +17896,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +44017,95.0,10.0,9.0,10.0,10.0,9.0,9.0,8,0.39 +29772,,,,,,,,0, +3559,85.0,9.0,8.0,9.0,9.0,9.0,9.0,42,1.84 +58806,91.0,10.0,9.0,10.0,10.0,10.0,9.0,28,1.22 +56305,,,,,,,,0, +31465,100.0,10.0,9.0,10.0,10.0,9.0,10.0,4,0.2 +20617,98.0,10.0,10.0,10.0,10.0,10.0,10.0,39,2.04 +40330,93.0,9.0,9.0,10.0,9.0,9.0,9.0,16,1.56 +147,97.0,10.0,10.0,10.0,10.0,10.0,10.0,70,3.13 +35579,100.0,9.0,9.0,10.0,10.0,8.0,9.0,2,0.11 +7276,80.0,7.0,7.0,8.0,8.0,8.0,7.0,4,0.18 +44660,,,,,,,,1,0.05 +52540,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.36 +50176,76.0,8.0,7.0,8.0,7.0,10.0,8.0,5,0.23 +9464,99.0,10.0,10.0,10.0,10.0,9.0,10.0,27,1.68 +6260,98.0,10.0,9.0,10.0,10.0,10.0,9.0,8,0.4 +48370,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.3 +48695,87.0,9.0,10.0,10.0,9.0,9.0,9.0,3,0.16 +11230,,,,,,,,0, +23076,85.0,9.0,9.0,9.0,9.0,9.0,8.0,19,0.9 +35071,97.0,10.0,10.0,10.0,10.0,10.0,9.0,36,1.65 +42511,94.0,10.0,9.0,10.0,10.0,10.0,9.0,32,1.43 +29452,91.0,9.0,9.0,10.0,9.0,10.0,9.0,42,1.95 +18395,,,,,,,,0, +16390,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +75273,,,,,,,,0, +76713,99.0,10.0,10.0,10.0,10.0,10.0,10.0,82,3.94 +56486,95.0,10.0,10.0,10.0,10.0,9.0,9.0,8,0.38 +16652,,,,,,,,0, +20624,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.23 +2177,,,,,,,,0, +42139,97.0,10.0,10.0,10.0,10.0,10.0,9.0,14,0.7 +51184,98.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.51 +69067,89.0,9.0,9.0,9.0,10.0,10.0,10.0,18,0.8 +74304,86.0,9.0,8.0,9.0,9.0,10.0,9.0,38,1.75 +65295,86.0,9.0,9.0,9.0,9.0,10.0,9.0,31,1.44 +71360,95.0,9.0,9.0,10.0,10.0,9.0,9.0,4,1.02 +8430,96.0,10.0,9.0,9.0,10.0,10.0,10.0,6,0.27 +33264,100.0,10.0,10.0,10.0,10.0,10.0,10.0,34,1.65 +70717,,,,,,,,0, +72564,95.0,10.0,10.0,10.0,10.0,10.0,9.0,120,5.37 +56181,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.14 +47150,100.0,10.0,10.0,9.0,10.0,9.0,10.0,2,0.1 +36966,94.0,9.0,10.0,10.0,10.0,10.0,10.0,105,4.65 +28027,93.0,10.0,10.0,10.0,10.0,10.0,9.0,97,4.33 +1761,93.0,10.0,9.0,10.0,10.0,10.0,9.0,111,5.06 +30661,89.0,9.0,9.0,10.0,9.0,10.0,9.0,122,5.43 +21485,90.0,9.0,9.0,10.0,10.0,10.0,9.0,138,6.16 +21972,98.0,10.0,10.0,10.0,10.0,9.0,9.0,20,1.11 +855,93.0,10.0,10.0,10.0,10.0,10.0,9.0,123,5.48 +857,84.0,8.0,9.0,9.0,9.0,9.0,8.0,16,0.75 +49480,92.0,10.0,9.0,10.0,9.0,10.0,9.0,145,6.48 +25204,100.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.36 +19508,,,,,,,,0, +20504,100.0,10.0,10.0,10.0,10.0,9.0,10.0,16,0.72 +47624,86.0,9.0,9.0,10.0,9.0,10.0,9.0,35,1.61 +10403,81.0,9.0,9.0,9.0,9.0,9.0,8.0,15,0.7 +37217,83.0,9.0,10.0,10.0,10.0,9.0,9.0,36,1.78 +12335,89.0,9.0,9.0,10.0,10.0,9.0,9.0,30,1.36 +14405,86.0,10.0,9.0,10.0,10.0,9.0,9.0,32,1.59 +22552,87.0,9.0,9.0,9.0,10.0,9.0,9.0,45,2.2 +4356,83.0,9.0,8.0,9.0,9.0,9.0,8.0,33,1.52 +74514,87.0,9.0,9.0,10.0,10.0,10.0,9.0,24,1.11 +18331,85.0,10.0,9.0,9.0,9.0,9.0,9.0,19,0.88 +62066,80.0,9.0,9.0,9.0,9.0,9.0,9.0,30,1.56 +71293,80.0,9.0,9.0,9.0,9.0,9.0,8.0,32,1.49 +26296,92.0,10.0,9.0,10.0,10.0,10.0,9.0,48,2.31 +48505,88.0,10.0,10.0,10.0,10.0,9.0,9.0,30,1.44 +33081,96.0,10.0,10.0,10.0,10.0,10.0,10.0,54,2.44 +6400,90.0,9.0,9.0,10.0,10.0,10.0,9.0,36,1.68 +33049,95.0,10.0,10.0,10.0,10.0,10.0,9.0,32,1.5 +66253,,,,,,,,0, +48188,91.0,9.0,9.0,10.0,10.0,10.0,9.0,7,0.42 +69458,98.0,9.0,10.0,10.0,10.0,10.0,9.0,11,0.84 +57715,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.45 +34149,95.0,9.0,10.0,10.0,10.0,10.0,10.0,11,0.51 +47410,99.0,10.0,10.0,10.0,10.0,10.0,10.0,21,0.95 +23799,,,,,,,,0, +11939,92.0,9.0,10.0,10.0,9.0,10.0,9.0,38,1.8 +38989,85.0,9.0,9.0,9.0,10.0,9.0,9.0,11,0.54 +37912,87.0,10.0,9.0,9.0,10.0,10.0,9.0,36,1.66 +40226,92.0,10.0,10.0,10.0,10.0,10.0,9.0,30,1.49 +44937,92.0,9.0,10.0,10.0,9.0,10.0,9.0,42,2.1 +40033,93.0,10.0,10.0,10.0,10.0,10.0,9.0,28,1.29 +23337,91.0,10.0,9.0,9.0,9.0,10.0,9.0,28,1.43 +32363,95.0,10.0,9.0,10.0,10.0,9.0,9.0,30,1.51 +72427,91.0,10.0,9.0,10.0,10.0,10.0,9.0,37,1.8 +15576,95.0,10.0,10.0,10.0,10.0,10.0,10.0,33,1.59 +68808,100.0,10.0,8.0,8.0,10.0,10.0,10.0,2,0.13 +52935,84.0,8.0,8.0,9.0,8.0,8.0,8.0,6,0.27 +21333,95.0,9.0,10.0,10.0,10.0,10.0,9.0,40,1.79 +21144,88.0,9.0,9.0,10.0,10.0,9.0,9.0,5,0.24 +19252,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.43 +28899,,,,,,,,0, +72350,93.0,10.0,9.0,10.0,10.0,10.0,10.0,21,0.93 +58598,90.0,9.0,9.0,10.0,10.0,10.0,9.0,116,5.29 +29740,100.0,9.0,9.0,9.0,9.0,9.0,9.0,2,2.0 +763,,,,,,,,0, +13722,90.0,10.0,10.0,10.0,10.0,9.0,8.0,2,0.14 +69674,95.0,10.0,10.0,10.0,10.0,10.0,10.0,40,1.88 +8229,91.0,10.0,10.0,10.0,10.0,10.0,9.0,50,2.23 +69509,95.0,10.0,10.0,10.0,10.0,10.0,10.0,50,2.46 +44783,91.0,9.0,9.0,9.0,10.0,10.0,9.0,50,2.3 +55429,98.0,10.0,10.0,10.0,10.0,9.0,10.0,20,1.1 +550,100.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.17 +42745,,,,,,,,0, +46521,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.18 +50342,95.0,10.0,10.0,10.0,10.0,10.0,10.0,59,2.65 +35125,90.0,10.0,10.0,10.0,9.0,9.0,9.0,2,0.09 +72668,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.18 +55179,75.0,8.0,9.0,10.0,10.0,10.0,8.0,8,0.38 +48979,98.0,10.0,10.0,10.0,10.0,10.0,10.0,62,2.9 +43105,88.0,9.0,9.0,9.0,10.0,9.0,9.0,12,0.53 +37307,97.0,10.0,9.0,10.0,10.0,9.0,10.0,14,0.65 +10128,97.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.38 +7212,,,,,,,,0, +75169,97.0,10.0,10.0,10.0,10.0,10.0,10.0,35,1.58 +56934,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.27 +2062,,,,,,,,1,0.1 +29673,92.0,9.0,9.0,10.0,10.0,9.0,9.0,23,1.15 +53615,100.0,10.0,9.0,10.0,10.0,9.0,9.0,2,0.12 +73100,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.11 +46100,88.0,9.0,8.0,10.0,10.0,9.0,9.0,22,1.04 +30324,80.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.05 +15836,100.0,10.0,9.0,10.0,9.0,10.0,10.0,3,0.23 +20556,88.0,10.0,8.0,10.0,10.0,9.0,9.0,5,0.23 +72309,60.0,8.0,4.0,10.0,10.0,6.0,8.0,1,0.07 +33668,97.0,10.0,9.0,10.0,10.0,10.0,9.0,26,1.21 +3333,92.0,10.0,10.0,10.0,10.0,10.0,9.0,26,1.18 +8871,96.0,10.0,10.0,10.0,10.0,10.0,9.0,66,2.97 +4688,100.0,9.0,10.0,10.0,10.0,10.0,10.0,4,0.2 +76634,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.09 +16071,90.0,9.0,9.0,9.0,10.0,10.0,9.0,6,0.3 +52618,89.0,9.0,9.0,10.0,10.0,10.0,9.0,19,0.87 +51519,99.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.17 +36935,78.0,9.0,7.0,10.0,9.0,10.0,8.0,17,0.88 +38860,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.81 +53048,,,,,,,,0, +46064,100.0,10.0,10.0,,,,,1,0.06 +37224,95.0,10.0,9.0,10.0,10.0,9.0,10.0,20,0.89 +72290,95.0,9.0,9.0,10.0,10.0,10.0,9.0,11,0.52 +18134,,,,,,,,1,0.04 +19485,83.0,9.0,9.0,9.0,9.0,9.0,8.0,7,0.42 +52359,84.0,8.0,8.0,9.0,9.0,9.0,8.0,10,0.48 +13177,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +13328,87.0,8.0,8.0,10.0,10.0,9.0,9.0,5,0.33 +21862,,,,,,,,0, +51418,,,,,,,,0, +6800,100.0,10.0,9.0,10.0,9.0,10.0,9.0,3,0.14 +32157,,,,,,,,1,0.16 +62168,95.0,10.0,8.0,10.0,10.0,10.0,9.0,4,0.18 +20544,93.0,9.0,9.0,10.0,9.0,9.0,9.0,12,0.57 +46794,95.0,10.0,10.0,10.0,10.0,10.0,10.0,36,1.84 +75249,,,,,,,,1,0.19 +43901,92.0,9.0,9.0,10.0,10.0,10.0,9.0,29,1.31 +34700,,,,,,,,1, +68705,81.0,9.0,8.0,9.0,9.0,9.0,8.0,15,0.92 +53277,,,,,,,,0, +66140,80.0,8.0,6.0,10.0,10.0,10.0,8.0,1,0.05 +4538,93.0,9.0,10.0,10.0,10.0,10.0,10.0,4,0.19 +47609,96.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.61 +53254,,,,,,,,1,0.05 +43391,99.0,10.0,10.0,10.0,10.0,10.0,10.0,87,4.11 +35280,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.23 +50458,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.18 +42255,97.0,10.0,10.0,10.0,10.0,10.0,10.0,62,2.8 +72455,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.11 +24140,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.04 +38555,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.42 +60822,,,,,,,,0, +3233,82.0,9.0,9.0,9.0,9.0,7.0,8.0,20,0.91 +32852,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.09 +75176,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.17 +54953,100.0,10.0,10.0,10.0,9.0,10.0,10.0,2,0.09 +38070,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.3 +46654,,,,,,,,0, +20996,95.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.31 +42749,87.0,9.0,9.0,9.0,10.0,9.0,9.0,29,1.41 +46078,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.21 +677,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.43 +76023,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.46 +68176,80.0,8.0,6.0,6.0,10.0,8.0,10.0,1,0.09 +17263,93.0,9.0,9.0,9.0,10.0,10.0,10.0,9,0.41 +63669,97.0,10.0,10.0,10.0,10.0,10.0,9.0,15,0.76 +14553,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.52 +62875,99.0,10.0,10.0,10.0,10.0,10.0,10.0,24,1.2 +68658,95.0,10.0,10.0,10.0,10.0,10.0,9.0,33,1.5 +10048,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.43 +19111,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,2.37 +52118,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.35 +40354,100.0,8.0,6.0,6.0,10.0,10.0,10.0,1,0.21 +71251,,,,,,,,0, +35096,60.0,8.0,8.0,4.0,6.0,4.0,6.0,1,0.08 +34420,95.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.23 +65195,94.0,10.0,10.0,10.0,10.0,9.0,9.0,28,1.31 +54539,89.0,9.0,9.0,9.0,10.0,9.0,8.0,34,1.59 +76233,,,,,,,,0, +13894,97.0,10.0,10.0,9.0,10.0,10.0,10.0,13,4.87 +63703,,,,,,,,0, +59883,86.0,9.0,8.0,10.0,10.0,9.0,9.0,14,0.77 +19736,95.0,9.0,9.0,9.0,9.0,10.0,10.0,15,0.68 +62863,97.0,10.0,9.0,9.0,10.0,10.0,10.0,6,0.54 +44767,97.0,10.0,10.0,10.0,10.0,10.0,9.0,32,1.45 +45507,100.0,9.0,10.0,10.0,10.0,9.0,10.0,3,0.15 +65758,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,0.1 +17062,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +15321,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +25353,83.0,9.0,9.0,10.0,9.0,9.0,9.0,101,4.6 +8299,100.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.5 +12988,89.0,10.0,9.0,10.0,9.0,9.0,9.0,72,3.26 +2693,,,,,,,,0, +9972,100.0,10.0,10.0,,10.0,,,2,0.09 +36807,99.0,10.0,10.0,10.0,10.0,10.0,10.0,23,1.08 +41386,95.0,10.0,9.0,10.0,10.0,10.0,10.0,17,1.21 +64899,,,,,,,,0, +51574,90.0,9.0,10.0,10.0,10.0,10.0,9.0,2,0.11 +11848,95.0,10.0,10.0,10.0,10.0,10.0,9.0,24,2.52 +75539,92.0,9.0,9.0,10.0,10.0,10.0,9.0,22,1.01 +31656,100.0,10.0,10.0,10.0,10.0,9.0,10.0,8,0.37 +33840,92.0,10.0,9.0,10.0,10.0,10.0,9.0,70,3.17 +3705,92.0,10.0,10.0,10.0,10.0,9.0,9.0,18,0.84 +12717,91.0,9.0,10.0,9.0,10.0,9.0,9.0,30,1.39 +55671,89.0,10.0,9.0,9.0,9.0,9.0,10.0,14,0.7 +1621,,,,,,,,0, +19355,93.0,9.0,9.0,10.0,9.0,9.0,9.0,4,0.2 +4546,85.0,8.0,8.0,10.0,10.0,9.0,9.0,4,0.48 +12864,100.0,9.0,9.0,10.0,10.0,8.0,9.0,3,0.15 +66640,93.0,10.0,9.0,10.0,10.0,9.0,9.0,12,0.59 +48744,60.0,7.0,4.0,10.0,10.0,10.0,6.0,12,0.57 +50962,,,,,,,,0, +66502,,,,,,,,0, +20963,80.0,6.0,6.0,6.0,6.0,8.0,6.0,1,0.05 +62778,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.09 +48507,94.0,10.0,10.0,10.0,10.0,10.0,9.0,29,1.48 +19607,,,,,,,,0, +75911,,,,,,,,0, +48714,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.23 +59257,96.0,10.0,10.0,10.0,10.0,10.0,10.0,30,1.87 +2199,82.0,9.0,8.0,10.0,10.0,10.0,9.0,97,4.81 +45097,95.0,9.0,9.0,10.0,10.0,9.0,10.0,13,0.64 +53795,,,,,,,,0, +50968,96.0,10.0,10.0,10.0,10.0,9.0,10.0,100,4.68 +5796,91.0,9.0,8.0,10.0,10.0,8.0,8.0,12,0.55 +8628,99.0,10.0,10.0,10.0,10.0,10.0,10.0,34,1.56 +11761,97.0,10.0,10.0,10.0,10.0,10.0,9.0,32,1.47 +68313,100.0,10.0,10.0,9.0,10.0,10.0,9.0,3,0.15 +11245,,,,,,,,0, +49569,,,,,,,,0, +28375,,,,,,,,0, +39016,96.0,10.0,10.0,10.0,10.0,10.0,10.0,48,2.32 +60160,100.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.35 +7316,,,,,,,,0, +45758,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +34947,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.09 +75144,93.0,9.0,9.0,10.0,10.0,9.0,9.0,49,2.27 +23064,95.0,10.0,10.0,10.0,10.0,10.0,10.0,36,1.66 +62673,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.09 +76330,100.0,9.0,10.0,10.0,10.0,10.0,10.0,2,0.1 +73340,100.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.6 +41543,99.0,10.0,10.0,10.0,10.0,10.0,10.0,62,2.9 +7644,,,,,,,,0, +20209,99.0,10.0,10.0,10.0,10.0,10.0,10.0,48,2.36 +41274,99.0,10.0,10.0,10.0,10.0,9.0,10.0,27,1.26 +52094,78.0,7.0,8.0,9.0,8.0,8.0,8.0,13,0.6 +27101,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.11 +10327,98.0,10.0,10.0,10.0,10.0,9.0,10.0,61,2.79 +60710,98.0,10.0,10.0,10.0,10.0,9.0,10.0,58,2.68 +72017,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +64594,,,,,,,,0, +36608,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.23 +11605,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.1 +5035,100.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.34 +75226,89.0,9.0,9.0,9.0,9.0,8.0,9.0,16,0.8 +71531,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.37 +16568,95.0,10.0,9.0,10.0,10.0,9.0,9.0,33,1.55 +9662,,,,,,,,0, +53477,93.0,9.0,9.0,10.0,9.0,9.0,9.0,5,0.23 +2294,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.43 +12936,,,,,,,,0, +3804,100.0,10.0,9.0,10.0,10.0,9.0,10.0,22,1.05 +1940,100.0,10.0,10.0,,10.0,,,2,0.12 +39360,,,,,,,,0, +20038,80.0,6.0,10.0,6.0,10.0,10.0,8.0,1,0.05 +76781,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.53 +9631,,,,,,,,0, +40669,93.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.15 +55619,98.0,10.0,10.0,10.0,10.0,9.0,10.0,43,2.0 +16641,97.0,10.0,9.0,10.0,10.0,9.0,10.0,36,1.79 +9826,91.0,9.0,9.0,10.0,10.0,9.0,9.0,46,2.16 +34650,92.0,9.0,9.0,10.0,10.0,10.0,9.0,24,1.21 +34314,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.55 +47001,95.0,10.0,10.0,10.0,10.0,10.0,9.0,44,2.29 +72688,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +1031,94.0,9.0,9.0,10.0,10.0,9.0,9.0,47,2.21 +42358,92.0,9.0,10.0,10.0,10.0,10.0,9.0,6,0.35 +17240,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +2735,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.79 +4479,90.0,7.0,9.0,10.0,10.0,10.0,8.0,2,0.16 +4564,83.0,9.0,8.0,10.0,9.0,9.0,9.0,29,1.34 +1347,97.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.37 +20182,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.51 +16738,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.19 +37098,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +41089,91.0,10.0,9.0,10.0,10.0,10.0,9.0,43,1.98 +29585,89.0,9.0,9.0,9.0,10.0,9.0,9.0,73,3.42 +30832,97.0,10.0,9.0,10.0,10.0,10.0,10.0,29,1.42 +7538,86.0,9.0,9.0,10.0,10.0,10.0,9.0,40,1.9 +64788,98.0,10.0,9.0,10.0,10.0,10.0,9.0,29,1.58 +42062,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +14637,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.41 +6334,,,,,,,,0, +29444,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.81 +46067,94.0,8.0,8.0,9.0,9.0,9.0,9.0,12,0.57 +58500,79.0,9.0,9.0,9.0,9.0,10.0,8.0,28,1.36 +30794,81.0,8.0,8.0,9.0,9.0,9.0,8.0,19,1.08 +30578,99.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.75 +35251,83.0,9.0,8.0,9.0,9.0,8.0,8.0,7,0.34 +23381,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.1 +23361,,,,,,,,0, +41903,82.0,9.0,8.0,9.0,9.0,9.0,9.0,35,1.66 +43567,94.0,10.0,10.0,10.0,10.0,10.0,9.0,34,1.75 +22048,91.0,10.0,10.0,10.0,10.0,10.0,9.0,35,1.87 +16914,93.0,10.0,10.0,10.0,10.0,10.0,9.0,38,1.97 +51081,94.0,10.0,9.0,10.0,10.0,10.0,9.0,47,2.19 +8763,95.0,10.0,10.0,10.0,10.0,9.0,10.0,71,3.39 +35769,97.0,10.0,10.0,10.0,10.0,10.0,10.0,38,2.04 +59483,,,,,,,,0, +17364,90.0,9.0,10.0,10.0,10.0,10.0,9.0,40,2.07 +6210,94.0,10.0,10.0,10.0,10.0,10.0,10.0,19,0.98 +57325,92.0,10.0,10.0,10.0,10.0,10.0,9.0,46,2.37 +9872,64.0,8.0,8.0,8.0,8.0,8.0,7.0,5,0.32 +25580,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.29 +68441,90.0,8.0,10.0,9.0,10.0,10.0,10.0,2,0.11 +28843,75.0,8.0,7.0,8.0,8.0,6.0,7.0,9,0.44 +41397,90.0,9.0,9.0,9.0,9.0,8.0,9.0,9,0.42 +41075,,,,,,,,0, +2476,90.0,9.0,9.0,9.0,10.0,10.0,9.0,4,0.24 +74904,94.0,10.0,9.0,9.0,9.0,10.0,9.0,7,0.39 +2807,100.0,9.0,10.0,9.0,9.0,9.0,9.0,3,0.15 +40364,99.0,10.0,10.0,10.0,10.0,9.0,10.0,121,5.65 +46568,92.0,9.0,9.0,10.0,10.0,10.0,9.0,20,0.98 +74067,94.0,9.0,10.0,10.0,10.0,10.0,9.0,28,1.35 +21743,87.0,9.0,9.0,9.0,9.0,9.0,9.0,98,4.59 +31406,98.0,10.0,10.0,9.0,10.0,9.0,9.0,20,1.05 +64132,97.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.3 +16378,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.25 +48671,100.0,10.0,10.0,,10.0,,,1,0.06 +61577,96.0,10.0,9.0,10.0,10.0,10.0,10.0,9,0.42 +53183,99.0,10.0,10.0,10.0,10.0,10.0,10.0,84,3.96 +38228,97.0,9.0,10.0,10.0,10.0,10.0,10.0,36,2.4 +68711,93.0,10.0,10.0,10.0,10.0,9.0,9.0,24,1.17 +37408,90.0,9.0,9.0,9.0,9.0,9.0,9.0,7,0.34 +37140,97.0,10.0,10.0,10.0,9.0,10.0,9.0,7,0.35 +76485,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.05 +20429,96.0,10.0,8.0,10.0,10.0,10.0,10.0,15,0.7 +35966,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +3538,97.0,10.0,9.0,10.0,10.0,10.0,9.0,20,0.96 +50425,99.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.06 +14713,,,,,,,,0, +55592,94.0,10.0,9.0,10.0,10.0,10.0,9.0,21,0.98 +70492,84.0,8.0,9.0,9.0,9.0,10.0,9.0,14,0.66 +39655,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +74230,87.0,9.0,9.0,9.0,9.0,9.0,9.0,30,1.48 +5548,89.0,9.0,8.0,8.0,8.0,9.0,9.0,9,0.43 +43371,85.0,9.0,9.0,9.0,9.0,10.0,8.0,31,1.5 +44314,90.0,9.0,9.0,9.0,9.0,9.0,9.0,31,1.46 +19434,96.0,10.0,9.0,10.0,10.0,10.0,10.0,24,1.41 +64251,91.0,9.0,9.0,9.0,10.0,9.0,9.0,42,2.15 +73153,,,,,,,,0, +7274,100.0,10.0,10.0,10.0,10.0,10.0,10.0,42,2.0 +68626,90.0,9.0,9.0,10.0,10.0,10.0,9.0,8,0.38 +37479,97.0,10.0,10.0,10.0,10.0,9.0,9.0,29,1.35 +52744,93.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.46 +61201,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +64085,95.0,10.0,10.0,10.0,9.0,10.0,10.0,4,0.68 +64886,96.0,10.0,9.0,10.0,10.0,10.0,9.0,11,0.53 +549,81.0,8.0,8.0,9.0,9.0,9.0,8.0,21,1.04 +42883,,,,,,,,0, +37378,80.0,8.0,9.0,8.0,9.0,8.0,9.0,5,0.25 +26270,99.0,10.0,10.0,10.0,10.0,10.0,10.0,70,3.27 +20103,97.0,10.0,10.0,10.0,10.0,10.0,10.0,39,1.89 +46221,,,,,,,,0, +21183,95.0,10.0,9.0,10.0,10.0,10.0,9.0,27,1.35 +7489,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.66 +24760,95.0,10.0,10.0,10.0,10.0,8.0,10.0,5,0.23 +27826,,,,,,,,0, +26452,,,,,,,,0, +48761,93.0,10.0,10.0,10.0,10.0,9.0,9.0,9,0.44 +62633,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,1.03 +20607,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.19 +15355,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.48 +72612,96.0,10.0,10.0,10.0,10.0,10.0,9.0,57,2.69 +39541,100.0,10.0,10.0,10.0,10.0,9.0,10.0,18,0.86 +22335,98.0,10.0,9.0,10.0,10.0,10.0,10.0,9,0.5 +74325,,,,,,,,0, +62894,93.0,10.0,9.0,10.0,10.0,9.0,9.0,23,1.41 +75862,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +10015,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.05 +43942,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.26 +52374,100.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.06 +163,88.0,9.0,8.0,10.0,10.0,10.0,8.0,5,0.24 +20477,95.0,9.0,10.0,9.0,8.0,10.0,9.0,11,0.54 +65913,,,,,,,,0, +28674,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.66 +57047,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.29 +36914,,,,,,,,0, +62195,86.0,9.0,9.0,9.0,9.0,9.0,9.0,77,3.63 +72982,96.0,9.0,10.0,10.0,10.0,9.0,9.0,28,1.34 +39143,96.0,10.0,9.0,10.0,10.0,10.0,9.0,17,0.86 +7456,,,,,,,,0, +56917,98.0,10.0,9.0,10.0,10.0,10.0,10.0,14,0.72 +570,92.0,9.0,9.0,10.0,10.0,10.0,9.0,41,2.0 +72043,90.0,9.0,9.0,10.0,10.0,10.0,10.0,2,0.1 +76794,80.0,9.0,9.0,9.0,10.0,8.0,8.0,3,0.17 +39482,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.1 +59597,94.0,9.0,9.0,10.0,10.0,10.0,9.0,15,0.73 +72796,88.0,9.0,9.0,9.0,9.0,9.0,8.0,43,2.11 +65939,95.0,10.0,10.0,10.0,10.0,10.0,9.0,50,2.4 +51936,92.0,10.0,10.0,10.0,10.0,10.0,9.0,32,2.02 +43494,97.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.29 +52932,88.0,9.0,9.0,9.0,10.0,9.0,9.0,114,5.45 +25289,100.0,10.0,9.0,10.0,10.0,10.0,10.0,20,1.08 +53601,100.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.36 +59149,91.0,10.0,9.0,9.0,10.0,9.0,9.0,136,6.48 +49918,100.0,10.0,10.0,10.0,10.0,6.0,10.0,1,0.05 +12210,,,,,,,,0, +51016,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.4 +27853,91.0,9.0,9.0,10.0,10.0,9.0,10.0,7,0.33 +5481,60.0,5.0,5.0,6.0,7.0,8.0,6.0,2,0.13 +34262,93.0,10.0,10.0,9.0,9.0,9.0,9.0,11,0.55 +55616,97.0,10.0,10.0,10.0,10.0,10.0,10.0,30,1.67 +34948,87.0,9.0,9.0,10.0,9.0,9.0,9.0,63,3.02 +38542,94.0,9.0,10.0,9.0,10.0,10.0,9.0,27,1.31 +19,,,,,,,,0, +58061,96.0,10.0,10.0,10.0,10.0,9.0,10.0,36,2.0 +70091,93.0,10.0,10.0,10.0,10.0,9.0,10.0,60,2.86 +51778,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +5709,100.0,10.0,10.0,8.0,10.0,8.0,10.0,2,0.11 +50952,89.0,9.0,10.0,9.0,9.0,8.0,9.0,12,0.68 +26540,92.0,10.0,9.0,10.0,10.0,10.0,9.0,10,0.51 +45714,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.54 +36035,,,,,,,,0, +2076,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.05 +43248,94.0,10.0,9.0,10.0,10.0,9.0,9.0,24,1.24 +1477,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.48 +26771,95.0,10.0,10.0,10.0,10.0,10.0,10.0,40,1.92 +43622,100.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.15 +55680,91.0,9.0,9.0,9.0,9.0,9.0,9.0,10,0.48 +75053,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.17 +48583,50.0,6.0,6.0,6.0,6.0,5.0,5.0,2,0.11 +57867,,,,,,,,0, +75322,,,,,,,,0, +36405,88.0,10.0,9.0,10.0,9.0,9.0,9.0,50,2.41 +46622,84.0,9.0,8.0,9.0,9.0,9.0,8.0,21,1.08 +67634,,,,,,,,0, +44903,80.0,10.0,8.0,10.0,10.0,8.0,10.0,1,0.05 +6081,100.0,10.0,10.0,10.0,10.0,10.0,6.0,1,0.05 +51071,94.0,9.0,9.0,10.0,10.0,9.0,9.0,20,0.99 +52435,92.0,10.0,10.0,9.0,8.0,10.0,8.0,5,0.25 +6414,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.5 +40532,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.1 +60274,,,,,,,,0, +56829,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +54606,,,,,,,,0, +43461,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.1 +19621,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.1 +9615,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +28606,98.0,10.0,10.0,10.0,10.0,10.0,9.0,91,4.39 +61871,98.0,10.0,10.0,10.0,10.0,10.0,9.0,49,2.33 +45640,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.24 +44760,98.0,10.0,10.0,10.0,10.0,10.0,10.0,33,1.67 +45292,,,,,,,,0, +54630,78.0,7.0,7.0,9.0,8.0,9.0,7.0,8,0.42 +43994,78.0,8.0,8.0,9.0,9.0,9.0,8.0,20,1.06 +1225,83.0,10.0,10.0,9.0,8.0,10.0,8.0,6,0.31 +21143,78.0,8.0,8.0,9.0,9.0,9.0,8.0,55,2.62 +50495,84.0,9.0,8.0,9.0,9.0,9.0,9.0,53,2.57 +24859,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.44 +6426,68.0,8.0,7.0,9.0,7.0,9.0,7.0,13,0.63 +53860,92.0,10.0,9.0,9.0,10.0,9.0,9.0,31,1.61 +45072,96.0,10.0,9.0,10.0,10.0,9.0,9.0,15,0.76 +24927,87.0,9.0,9.0,9.0,10.0,9.0,9.0,115,5.49 +32781,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.53 +73297,57.0,7.0,8.0,8.0,7.0,9.0,5.0,8,0.39 +73695,92.0,10.0,9.0,10.0,10.0,10.0,10.0,34,1.85 +24340,92.0,9.0,9.0,8.0,10.0,10.0,9.0,18,0.88 +5127,100.0,10.0,10.0,10.0,10.0,9.0,10.0,19,1.12 +38763,96.0,10.0,9.0,10.0,10.0,9.0,10.0,16,0.79 +9353,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.67 +35028,96.0,10.0,10.0,10.0,10.0,9.0,10.0,97,4.63 +52171,97.0,10.0,10.0,10.0,10.0,10.0,10.0,84,4.1 +22782,85.0,9.0,9.0,9.0,9.0,9.0,9.0,44,2.13 +22103,99.0,10.0,10.0,10.0,10.0,10.0,10.0,82,4.71 +28885,99.0,10.0,10.0,10.0,10.0,10.0,10.0,59,2.82 +35330,80.0,9.0,7.0,10.0,10.0,10.0,9.0,5,0.24 +43609,87.0,10.0,7.0,10.0,9.0,9.0,8.0,3,0.15 +59058,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.16 +38664,100.0,10.0,9.0,10.0,10.0,9.0,9.0,2,0.1 +32254,94.0,10.0,9.0,10.0,10.0,10.0,9.0,51,2.81 +20497,96.0,10.0,10.0,10.0,10.0,10.0,9.0,38,1.84 +22835,97.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.64 +14496,98.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.46 +62219,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.25 +6219,,,,,,,,0, +44446,89.0,9.0,9.0,10.0,10.0,10.0,9.0,42,2.02 +74944,78.0,7.0,7.0,9.0,9.0,8.0,8.0,8,0.39 +55045,,,,,,,,0, +50956,99.0,10.0,10.0,10.0,10.0,10.0,10.0,54,2.83 +61301,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.46 +44842,,,,,,,,0, +16189,97.0,10.0,10.0,10.0,10.0,8.0,9.0,8,0.42 +45998,100.0,10.0,10.0,10.0,10.0,9.0,10.0,16,0.82 +46419,80.0,8.0,6.0,10.0,10.0,8.0,8.0,1,0.05 +52406,75.0,9.0,9.0,9.0,9.0,10.0,8.0,25,1.21 +7936,,,,,,,,0, +28393,93.0,9.0,10.0,10.0,10.0,8.0,10.0,34,2.41 +50612,97.0,10.0,10.0,10.0,10.0,10.0,10.0,25,1.22 +70372,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.24 +11609,98.0,10.0,10.0,10.0,10.0,9.0,10.0,25,1.21 +24872,93.0,9.0,9.0,9.0,9.0,10.0,9.0,3,0.15 +69003,93.0,9.0,9.0,8.0,7.0,8.0,8.0,6,0.3 +73360,97.0,10.0,10.0,9.0,9.0,10.0,10.0,7,0.34 +9986,97.0,10.0,9.0,10.0,10.0,10.0,10.0,23,1.13 +36553,94.0,10.0,9.0,10.0,10.0,9.0,9.0,26,1.31 +42678,88.0,9.0,9.0,10.0,10.0,10.0,9.0,28,1.4 +58482,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +46147,88.0,9.0,9.0,10.0,9.0,9.0,9.0,135,6.6 +46451,87.0,9.0,9.0,9.0,9.0,9.0,9.0,77,3.71 +2323,,,,,,,,0, +51427,97.0,10.0,10.0,9.0,10.0,9.0,10.0,22,1.52 +44615,87.0,10.0,8.0,10.0,10.0,10.0,9.0,3,0.18 +23281,95.0,10.0,10.0,10.0,10.0,10.0,9.0,40,1.97 +37010,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.15 +39676,95.0,10.0,10.0,9.0,10.0,9.0,9.0,15,0.92 +73280,90.0,9.0,9.0,9.0,10.0,10.0,9.0,14,1.06 +42378,,,,,,,,0, +25072,100.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.23 +53321,91.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.48 +56929,92.0,9.0,10.0,10.0,10.0,9.0,9.0,12,0.59 +13628,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.16 +2206,96.0,10.0,10.0,10.0,10.0,10.0,9.0,22,1.08 +34302,97.0,9.0,9.0,10.0,10.0,10.0,9.0,18,1.11 +14708,95.0,10.0,7.0,10.0,9.0,10.0,10.0,4,0.2 +37240,,,,,,,,0, +57408,90.0,9.0,9.0,10.0,10.0,10.0,9.0,24,1.37 +30935,83.0,9.0,8.0,8.0,8.0,10.0,9.0,10,0.48 +49093,79.0,9.0,8.0,9.0,9.0,9.0,9.0,90,4.35 +51073,,,,,,,,0, +64297,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +67173,,,,,,,,0, +21054,98.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.86 +28755,96.0,10.0,10.0,10.0,10.0,8.0,9.0,53,2.78 +66805,96.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.8 +67177,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.17 +50634,80.0,9.0,10.0,7.0,10.0,10.0,9.0,4,0.22 +5186,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +74457,95.0,9.0,10.0,10.0,10.0,9.0,9.0,30,1.53 +66790,,,,,,,,0, +6519,87.0,9.0,7.0,9.0,9.0,9.0,8.0,3,0.15 +60534,90.0,9.0,9.0,9.0,9.0,10.0,8.0,8,0.45 +4246,93.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.35 +46842,100.0,10.0,10.0,10.0,10.0,10.0,10.0,48,2.75 +64487,88.0,9.0,9.0,10.0,9.0,9.0,9.0,33,1.67 +11380,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.1 +19415,99.0,10.0,10.0,10.0,10.0,10.0,10.0,32,1.74 +21122,98.0,10.0,10.0,10.0,10.0,9.0,9.0,11,0.55 +40469,97.0,10.0,9.0,10.0,10.0,9.0,10.0,8,0.43 +690,94.0,10.0,8.0,9.0,9.0,10.0,10.0,11,0.54 +39615,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.15 +76863,80.0,9.0,9.0,10.0,10.0,10.0,9.0,3,0.15 +39540,,,,,,,,0, +36878,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.29 +26626,96.0,10.0,9.0,10.0,10.0,10.0,9.0,18,0.89 +10141,97.0,10.0,10.0,10.0,10.0,10.0,10.0,69,3.4 +73574,,,,,,,,0, +23548,98.0,9.0,10.0,10.0,10.0,10.0,10.0,8,0.46 +3314,,,,,,,,0, +68799,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +3346,,,,,,,,0, +25869,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.2 +19675,94.0,10.0,10.0,10.0,10.0,9.0,9.0,52,2.91 +6658,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,1.01 +64032,92.0,9.0,9.0,10.0,10.0,9.0,9.0,103,5.35 +9226,100.0,10.0,8.0,10.0,8.0,10.0,10.0,2,0.1 +54187,97.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.26 +32419,,,,,,,,0, +8474,,,,,,,,0, +47042,80.0,10.0,10.0,6.0,6.0,10.0,8.0,1,0.06 +39061,90.0,9.0,10.0,9.0,10.0,10.0,9.0,4,0.21 +59961,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.15 +58856,98.0,10.0,10.0,10.0,10.0,10.0,10.0,73,3.58 +11516,93.0,10.0,10.0,10.0,10.0,10.0,10.0,59,2.91 +67226,87.0,8.0,9.0,9.0,9.0,10.0,9.0,11,0.55 +11102,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.61 +64778,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.11 +41666,99.0,10.0,10.0,10.0,10.0,10.0,10.0,30,1.49 +58256,90.0,8.0,7.0,6.0,6.0,8.0,8.0,2,0.1 +35342,100.0,10.0,10.0,,,,,1,0.05 +26878,96.0,10.0,9.0,9.0,10.0,10.0,9.0,15,0.86 +21532,100.0,8.0,8.0,8.0,8.0,8.0,8.0,1,0.06 +37644,90.0,10.0,10.0,10.0,9.0,9.0,10.0,2,0.1 +28570,97.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.81 +22488,83.0,9.0,8.0,10.0,9.0,8.0,8.0,8,0.4 +836,,,,,,,,0, +35865,50.0,5.0,4.0,8.0,8.0,8.0,6.0,4,0.2 +75056,85.0,9.0,9.0,9.0,10.0,9.0,8.0,19,1.0 +15552,92.0,9.0,8.0,7.0,9.0,10.0,8.0,5,0.64 +63416,100.0,10.0,7.0,8.0,7.0,9.0,10.0,3,0.15 +64225,82.0,9.0,8.0,9.0,9.0,9.0,8.0,53,2.61 +2835,100.0,10.0,10.0,10.0,9.0,9.0,10.0,17,0.92 +7633,94.0,10.0,9.0,9.0,10.0,10.0,9.0,24,1.23 +29693,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.09 +69559,97.0,10.0,10.0,10.0,10.0,10.0,10.0,42,2.3 +4180,92.0,9.0,8.0,10.0,10.0,10.0,10.0,6,0.29 +75143,90.0,9.0,9.0,10.0,10.0,10.0,9.0,4,0.23 +73277,,,,,,,,7,0.34 +17795,98.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.87 +47834,,,,,,,,0, +34135,80.0,8.0,8.0,10.0,10.0,10.0,8.0,2,0.16 +10156,,,,,,,,0, +60702,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.6 +63328,92.0,9.0,9.0,10.0,10.0,10.0,9.0,30,1.58 +14571,99.0,10.0,10.0,10.0,10.0,9.0,10.0,24,1.22 +2105,,,,,,,,0, +51690,93.0,10.0,10.0,10.0,10.0,10.0,9.0,17,0.85 +57624,98.0,10.0,10.0,10.0,10.0,9.0,10.0,101,5.32 +73609,,,,,,,,0, +4191,96.0,10.0,10.0,10.0,10.0,10.0,9.0,81,4.04 +46004,100.0,10.0,10.0,9.0,9.0,9.0,9.0,2,0.12 +70560,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.69 +65426,99.0,10.0,10.0,10.0,10.0,10.0,10.0,21,1.07 +72729,95.0,10.0,10.0,10.0,10.0,9.0,9.0,60,2.12 +9777,98.0,10.0,10.0,10.0,10.0,9.0,10.0,37,1.84 +8869,100.0,9.0,10.0,9.0,10.0,10.0,8.0,3,0.22 +19112,95.0,9.0,9.0,10.0,10.0,9.0,9.0,39,1.99 +3723,88.0,10.0,8.0,10.0,10.0,10.0,10.0,6,0.37 +10292,91.0,9.0,10.0,9.0,9.0,10.0,9.0,33,1.63 +46774,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +65425,,,,,,,,0, +17453,91.0,9.0,9.0,9.0,10.0,10.0,9.0,38,1.99 +71487,,,,,,,,0, +61534,90.0,10.0,9.0,10.0,8.0,10.0,9.0,2,0.12 +8283,98.0,10.0,10.0,10.0,10.0,10.0,10.0,41,2.12 +33793,89.0,9.0,9.0,10.0,10.0,10.0,9.0,20,1.05 +32860,97.0,10.0,10.0,10.0,10.0,9.0,10.0,33,1.68 +68147,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.21 +48109,99.0,10.0,10.0,10.0,10.0,9.0,9.0,43,2.1 +37809,98.0,10.0,10.0,10.0,10.0,10.0,10.0,74,3.72 +123,91.0,9.0,9.0,9.0,9.0,10.0,9.0,25,1.27 +37784,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.2 +53007,86.0,9.0,10.0,10.0,10.0,9.0,9.0,22,1.09 +23350,,,,,,,,0, +36124,88.0,9.0,8.0,10.0,10.0,10.0,9.0,7,0.35 +10211,80.0,9.0,8.0,9.0,9.0,9.0,9.0,25,1.27 +73347,99.0,10.0,10.0,10.0,10.0,9.0,10.0,61,3.04 +23627,,,,,,,,1,0.06 +57663,100.0,10.0,10.0,10.0,10.0,10.0,10.0,23,1.16 +41939,96.0,10.0,9.0,10.0,10.0,9.0,9.0,19,1.02 +61178,,,,,,,,0, +20479,95.0,9.0,9.0,10.0,10.0,9.0,9.0,22,1.1 +70685,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.55 +23224,,,,,,,,0, +22745,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.17 +14669,,,,,,,,0, +41162,91.0,9.0,9.0,10.0,10.0,10.0,9.0,43,2.15 +49889,80.0,8.0,8.0,8.0,8.0,9.0,9.0,16,0.8 +5586,,,,,,,,0, +14840,53.0,10.0,10.0,10.0,7.0,8.0,8.0,3,0.2 +18384,100.0,10.0,10.0,10.0,10.0,10.0,10.0,52,2.58 +58863,91.0,8.0,9.0,8.0,9.0,9.0,8.0,8,0.41 +37791,91.0,9.0,9.0,9.0,9.0,10.0,9.0,52,2.74 +49594,100.0,10.0,9.0,9.0,10.0,9.0,10.0,3,0.15 +3195,88.0,8.0,9.0,9.0,10.0,10.0,8.0,5,0.27 +54894,,,,,,,,0, +42266,100.0,10.0,9.0,10.0,10.0,9.0,10.0,6,0.38 +44800,98.0,10.0,10.0,10.0,10.0,10.0,9.0,9,0.47 +1343,89.0,9.0,9.0,9.0,9.0,8.0,9.0,25,1.23 +66794,95.0,10.0,10.0,10.0,10.0,10.0,9.0,27,1.35 +65486,86.0,9.0,9.0,9.0,8.0,9.0,9.0,24,1.23 +44568,86.0,9.0,8.0,10.0,10.0,10.0,9.0,10,0.52 +18738,,,,,,,,0, +63732,87.0,7.0,8.0,7.0,9.0,10.0,8.0,3,0.24 +54022,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +18972,84.0,9.0,7.0,10.0,10.0,9.0,9.0,11,0.57 +20503,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.77 +72641,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.6 +3851,99.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.84 +9681,99.0,10.0,10.0,10.0,10.0,10.0,10.0,30,1.6 +37852,99.0,10.0,10.0,10.0,10.0,10.0,10.0,42,2.15 +32228,93.0,9.0,9.0,10.0,10.0,9.0,9.0,3,0.15 +51469,,,,,,,,0, +63686,100.0,10.0,10.0,10.0,10.0,9.0,10.0,10,0.5 +57926,98.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.95 +28649,100.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.98 +13377,,,,,,,,0, +47270,85.0,9.0,10.0,10.0,9.0,10.0,9.0,43,2.18 +73643,96.0,10.0,10.0,10.0,10.0,8.0,9.0,16,0.8 +33187,90.0,10.0,9.0,10.0,9.0,10.0,9.0,24,1.19 +60079,80.0,10.0,8.0,10.0,10.0,8.0,10.0,2,0.25 +38726,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.48 +37758,98.0,10.0,9.0,10.0,10.0,10.0,10.0,13,0.76 +15754,,,,,,,,1,0.12 +15809,,,,,,,,0, +7746,91.0,9.0,9.0,9.0,9.0,10.0,8.0,9,0.51 +72969,,,,,,,,0, +3004,72.0,8.0,9.0,7.0,7.0,9.0,8.0,19,1.04 +80,80.0,9.0,9.0,9.0,8.0,9.0,9.0,7,0.37 +28045,80.0,8.0,9.0,7.0,6.0,9.0,8.0,6,0.33 +18059,73.0,9.0,9.0,5.0,7.0,9.0,8.0,3,0.16 +10388,69.0,7.0,8.0,8.0,7.0,9.0,7.0,29,1.48 +67157,63.0,8.0,8.0,7.0,7.0,9.0,7.0,7,0.35 +75150,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +5116,87.0,8.0,9.0,10.0,10.0,10.0,9.0,3,0.16 +50170,90.0,10.0,9.0,9.0,9.0,10.0,10.0,11,0.61 +63673,97.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.41 +35229,90.0,10.0,9.0,10.0,10.0,10.0,9.0,21,1.08 +70322,98.0,10.0,10.0,10.0,10.0,10.0,10.0,92,4.73 +6402,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.36 +4535,97.0,10.0,10.0,10.0,10.0,10.0,9.0,8,0.44 +51067,,,,,,,,0, +55421,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.21 +44504,100.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.32 +38956,98.0,10.0,10.0,10.0,10.0,10.0,9.0,40,2.05 +48773,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +47476,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.37 +57610,98.0,10.0,10.0,10.0,10.0,10.0,10.0,38,2.9 +61129,65.0,7.0,7.0,10.0,9.0,9.0,7.0,4,0.23 +55175,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.1 +31474,,,,,,,,0, +55478,,,,,,,,0, +70916,94.0,10.0,10.0,10.0,10.0,10.0,9.0,32,1.75 +47756,,,,,,,,0, +12648,,,,,,,,0, +40554,88.0,10.0,9.0,6.0,10.0,9.0,9.0,5,0.26 +23428,91.0,9.0,9.0,9.0,10.0,9.0,10.0,39,2.0 +49911,,,,,,,,0, +37864,100.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.53 +24012,98.0,10.0,10.0,10.0,10.0,10.0,10.0,24,1.27 +50647,94.0,9.0,9.0,10.0,10.0,9.0,10.0,23,1.48 +35700,93.0,10.0,10.0,9.0,9.0,10.0,9.0,15,0.76 +3097,93.0,9.0,10.0,10.0,10.0,9.0,9.0,12,0.61 +74824,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.05 +9479,,,,,,,,0, +15570,96.0,10.0,10.0,10.0,10.0,9.0,9.0,62,3.2 +17504,93.0,9.0,9.0,9.0,10.0,10.0,9.0,27,1.51 +58106,100.0,10.0,9.0,10.0,10.0,10.0,9.0,4,0.48 +33925,,,,,,,,0, +6734,93.0,9.0,9.0,8.0,9.0,10.0,9.0,8,0.48 +7215,,,,,,,,0, +48680,89.0,9.0,9.0,10.0,10.0,8.0,9.0,13,0.68 +60676,100.0,10.0,10.0,,,,,1,0.06 +11696,100.0,10.0,10.0,10.0,10.0,10.0,10.0,24,1.23 +28167,100.0,10.0,9.0,10.0,10.0,9.0,10.0,2,0.1 +74699,92.0,9.0,10.0,9.0,10.0,9.0,9.0,10,0.65 +9791,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.33 +60738,100.0,10.0,10.0,10.0,10.0,9.0,10.0,27,1.46 +47501,78.0,8.0,8.0,9.0,9.0,8.0,7.0,10,0.54 +32249,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.5 +58719,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.2 +1358,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,0.12 +61661,95.0,10.0,10.0,9.0,10.0,10.0,10.0,38,1.96 +56483,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.51 +52741,100.0,10.0,10.0,10.0,10.0,9.0,9.0,11,0.87 +22709,96.0,9.0,9.0,10.0,8.0,9.0,9.0,6,0.32 +25828,100.0,10.0,10.0,10.0,10.0,9.0,9.0,7,0.38 +61814,98.0,10.0,10.0,9.0,10.0,10.0,10.0,9,0.46 +27956,80.0,8.0,8.0,9.0,8.0,10.0,8.0,8,0.42 +57535,98.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.67 +71949,86.0,8.0,8.0,9.0,9.0,9.0,8.0,10,0.79 +44705,93.0,9.0,9.0,10.0,10.0,9.0,9.0,73,3.82 +47413,96.0,10.0,10.0,10.0,9.0,10.0,9.0,5,0.54 +15647,98.0,10.0,10.0,10.0,10.0,10.0,9.0,34,1.73 +59518,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.29 +42161,98.0,10.0,10.0,10.0,10.0,9.0,10.0,16,0.82 +56344,99.0,10.0,10.0,10.0,10.0,10.0,10.0,38,2.21 +54103,84.0,8.0,8.0,9.0,9.0,9.0,8.0,27,1.45 +69809,27.0,4.0,5.0,3.0,2.0,4.0,3.0,3,0.23 +74526,98.0,10.0,10.0,10.0,10.0,10.0,10.0,50,2.56 +2234,,,,,,,,0, +39411,100.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.54 +21416,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.06 +75637,90.0,9.0,8.0,9.0,10.0,10.0,9.0,92,4.98 +30707,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.15 +44914,80.0,8.0,8.0,8.0,8.0,9.0,8.0,7,0.39 +7814,73.0,7.0,7.0,7.0,7.0,7.0,7.0,3,0.28 +68723,80.0,8.0,8.0,8.0,7.0,8.0,8.0,10,0.52 +54087,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +23172,,,,,,,,0, +40348,77.0,8.0,7.0,9.0,9.0,9.0,8.0,25,1.28 +6458,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.05 +33976,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.66 +3014,82.0,9.0,9.0,9.0,9.0,8.0,8.0,31,1.8 +1586,93.0,10.0,10.0,10.0,10.0,10.0,10.0,30,1.59 +73992,100.0,9.0,9.0,10.0,10.0,10.0,9.0,6,0.32 +34625,98.0,10.0,10.0,10.0,10.0,10.0,9.0,16,0.89 +72212,83.0,9.0,9.0,10.0,9.0,10.0,8.0,114,5.89 +36731,,,,,,,,0, +37941,,,,,,,,1,0.05 +56106,,,,,,,,0, +36687,87.0,9.0,8.0,9.0,9.0,10.0,9.0,33,1.67 +69517,,,,,,,,0, +21638,98.0,10.0,10.0,10.0,10.0,9.0,10.0,10,0.55 +20685,81.0,9.0,8.0,10.0,9.0,10.0,8.0,17,1.01 +37766,85.0,8.0,8.0,8.0,9.0,10.0,9.0,32,1.72 +14121,89.0,9.0,8.0,10.0,10.0,9.0,9.0,42,2.11 +48052,,,,,,,,0, +63806,98.0,10.0,10.0,9.0,10.0,9.0,10.0,19,2.1 +71748,,,,,,,,0, +564,63.0,5.0,4.0,7.0,7.0,7.0,6.0,9,0.46 +52414,100.0,10.0,7.0,10.0,10.0,10.0,9.0,3,0.45 +7511,99.0,10.0,10.0,10.0,10.0,10.0,10.0,18,0.93 +53171,97.0,10.0,10.0,10.0,10.0,10.0,10.0,23,1.19 +51909,93.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.33 +25952,86.0,9.0,8.0,9.0,9.0,10.0,9.0,24,1.39 +70661,92.0,9.0,9.0,10.0,9.0,10.0,9.0,29,1.57 +56986,94.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.67 +66929,,,,,,,,0, +14156,96.0,10.0,10.0,10.0,10.0,10.0,10.0,24,1.37 +76001,99.0,10.0,10.0,10.0,10.0,10.0,10.0,36,1.83 +61522,88.0,9.0,9.0,10.0,9.0,9.0,9.0,17,0.87 +71575,93.0,10.0,10.0,10.0,9.0,10.0,9.0,3,0.18 +74812,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +48015,72.0,7.0,7.0,9.0,9.0,7.0,7.0,6,0.34 +65391,97.0,10.0,10.0,10.0,10.0,10.0,10.0,23,1.25 +68879,97.0,10.0,10.0,9.0,10.0,9.0,9.0,7,0.36 +43803,,,,,,,,0, +53182,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.45 +34561,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.26 +70698,,,,,,,,0, +75855,,,,,,,,0, +38434,40.0,6.0,10.0,4.0,6.0,10.0,4.0,1,0.05 +50692,,,,,,,,0, +43351,,,,,,,,0, +44168,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.17 +66412,,,,,,,,0, +18783,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.11 +47261,80.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.06 +72588,,,,,,,,0, +63049,83.0,9.0,9.0,9.0,9.0,10.0,8.0,33,1.72 +39190,88.0,10.0,8.0,8.0,9.0,8.0,9.0,8,0.43 +9629,97.0,10.0,10.0,10.0,10.0,10.0,10.0,34,1.74 +44724,76.0,9.0,8.0,9.0,10.0,8.0,8.0,5,0.25 +41868,88.0,9.0,9.0,9.0,9.0,10.0,9.0,31,2.02 +30535,93.0,10.0,10.0,10.0,10.0,9.0,9.0,107,5.49 +29233,92.0,10.0,9.0,10.0,10.0,10.0,9.0,13,0.67 +26191,93.0,10.0,9.0,10.0,10.0,9.0,9.0,6,0.38 +76777,95.0,10.0,10.0,9.0,10.0,9.0,9.0,15,0.76 +18472,99.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.32 +40529,98.0,10.0,10.0,10.0,10.0,9.0,10.0,16,0.83 +43612,93.0,10.0,10.0,9.0,10.0,9.0,9.0,11,0.58 +66507,90.0,9.0,10.0,9.0,10.0,10.0,9.0,27,1.43 +5873,77.0,8.0,8.0,8.0,8.0,9.0,9.0,6,0.31 +60929,,,,,,,,0, +48790,80.0,8.0,10.0,10.0,10.0,8.0,6.0,1,0.05 +17381,99.0,10.0,10.0,10.0,10.0,9.0,10.0,30,1.6 +14104,,,,,,,,0, +7127,,,,,,,,0, +44883,95.0,10.0,10.0,10.0,10.0,10.0,9.0,53,2.69 +47446,97.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.43 +17454,100.0,10.0,10.0,10.0,10.0,9.0,10.0,44,2.25 +60849,98.0,10.0,9.0,10.0,10.0,9.0,10.0,12,0.77 +42725,98.0,10.0,9.0,10.0,10.0,9.0,10.0,26,1.33 +35177,98.0,10.0,10.0,10.0,10.0,10.0,10.0,30,1.64 +33024,,,,,,,,0, +35661,90.0,10.0,8.0,10.0,10.0,9.0,9.0,2,0.1 +31727,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,1.08 +31957,97.0,10.0,10.0,10.0,10.0,9.0,10.0,62,3.37 +32279,80.0,9.0,9.0,10.0,10.0,10.0,10.0,2,0.11 +49482,93.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.16 +65157,97.0,10.0,10.0,10.0,10.0,10.0,10.0,77,4.23 +74381,100.0,9.0,9.0,10.0,10.0,10.0,9.0,4,0.24 +28858,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.74 +12056,93.0,9.0,9.0,9.0,9.0,9.0,10.0,6,0.33 +28829,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.95 +66043,60.0,6.0,4.0,6.0,6.0,4.0,4.0,1,0.05 +58023,99.0,10.0,10.0,10.0,10.0,10.0,10.0,17,0.88 +60953,92.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.32 +32185,,,,,,,,0, +67014,,,,,,,,0, +20432,,,,,,,,0, +50329,99.0,10.0,10.0,10.0,10.0,9.0,10.0,24,1.24 +58624,90.0,9.0,9.0,10.0,10.0,10.0,10.0,4,0.26 +52328,100.0,10.0,10.0,8.0,10.0,10.0,10.0,4,0.21 +6870,92.0,9.0,10.0,10.0,10.0,10.0,10.0,6,0.31 +61332,100.0,10.0,10.0,10.0,10.0,9.0,9.0,18,0.98 +21758,98.0,10.0,10.0,9.0,10.0,10.0,9.0,13,0.68 +8478,99.0,10.0,10.0,10.0,10.0,10.0,10.0,68,3.54 +7233,100.0,10.0,10.0,10.0,10.0,9.0,9.0,21,1.09 +72297,79.0,8.0,6.0,9.0,9.0,10.0,8.0,23,1.18 +55595,,,,,,,,0, +53599,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.28 +22847,90.0,9.0,9.0,10.0,10.0,9.0,9.0,101,5.18 +31013,89.0,10.0,9.0,10.0,9.0,10.0,9.0,9,0.52 +75105,89.0,9.0,9.0,10.0,10.0,9.0,9.0,74,3.87 +72011,95.0,9.0,10.0,10.0,10.0,10.0,9.0,12,0.85 +784,100.0,10.0,10.0,10.0,8.0,8.0,8.0,1,0.05 +62221,,,,,,,,0, +66185,99.0,10.0,10.0,10.0,10.0,10.0,10.0,30,1.57 +29590,95.0,9.0,10.0,9.0,10.0,10.0,9.0,9,0.49 +64743,60.0,10.0,2.0,8.0,10.0,6.0,6.0,2,0.13 +34454,97.0,10.0,10.0,10.0,10.0,10.0,9.0,25,1.29 +75889,,,,,,,,0, +5347,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.41 +42945,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.23 +4883,100.0,10.0,10.0,9.0,10.0,10.0,10.0,4,0.22 +7578,97.0,10.0,10.0,10.0,10.0,10.0,10.0,62,3.56 +24911,80.0,8.0,8.0,10.0,10.0,10.0,10.0,1,0.06 +30390,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.16 +21067,92.0,10.0,9.0,10.0,10.0,9.0,9.0,5,0.98 +13401,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.21 +11329,96.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.72 +28203,99.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.25 +50287,98.0,10.0,10.0,10.0,10.0,9.0,10.0,16,1.11 +51075,85.0,9.0,9.0,9.0,9.0,9.0,8.0,30,1.67 +72155,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.21 +47263,96.0,10.0,9.0,10.0,10.0,10.0,10.0,39,2.06 +58424,95.0,10.0,9.0,10.0,10.0,10.0,10.0,48,2.5 +65362,98.0,10.0,10.0,10.0,10.0,10.0,9.0,10,0.53 +69792,96.0,10.0,10.0,10.0,10.0,9.0,9.0,40,2.09 +54872,,,,,,,,0, +27204,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.16 +39205,92.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.29 +13465,100.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.37 +44997,,,,,,,,0, +48447,,,,,,,,0, +15512,,,,,,,,0, +38589,93.0,10.0,9.0,10.0,10.0,9.0,9.0,27,1.44 +30372,89.0,9.0,9.0,10.0,10.0,9.0,9.0,31,1.77 +8121,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.48 +72923,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.61 +37256,96.0,10.0,9.0,10.0,10.0,9.0,9.0,5,0.29 +74322,97.0,10.0,10.0,10.0,10.0,10.0,10.0,48,3.22 +2506,85.0,9.0,10.0,10.0,10.0,10.0,9.0,11,0.61 +57809,93.0,10.0,10.0,9.0,9.0,9.0,9.0,31,1.61 +39728,100.0,10.0,10.0,9.0,10.0,10.0,10.0,7,0.38 +310,89.0,9.0,10.0,9.0,9.0,10.0,9.0,17,0.9 +55453,93.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.65 +39961,92.0,9.0,10.0,10.0,10.0,10.0,9.0,10,0.59 +17735,87.0,9.0,8.0,9.0,9.0,10.0,9.0,19,1.06 +604,96.0,10.0,9.0,10.0,10.0,9.0,10.0,43,2.26 +20359,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +12137,92.0,10.0,9.0,10.0,10.0,10.0,9.0,45,2.35 +38502,97.0,10.0,10.0,10.0,10.0,10.0,10.0,16,0.87 +20757,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.17 +41804,91.0,9.0,9.0,10.0,10.0,9.0,9.0,17,0.93 +75554,,,,,,,,0, +7801,89.0,9.0,10.0,10.0,10.0,9.0,9.0,69,3.58 +55003,89.0,9.0,9.0,10.0,10.0,9.0,9.0,106,5.48 +66638,80.0,7.0,10.0,10.0,10.0,5.0,8.0,2,0.12 +70142,,,,,,,,0, +45993,85.0,9.0,8.0,9.0,9.0,9.0,8.0,15,0.84 +66159,96.0,10.0,10.0,10.0,10.0,9.0,10.0,25,4.49 +32291,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.11 +26015,94.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.53 +62902,80.0,10.0,8.0,10.0,10.0,8.0,10.0,2,0.1 +68110,80.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.06 +13830,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.13 +57792,,,,,,,,0, +28432,,,,,,,,0, +40396,94.0,10.0,9.0,10.0,10.0,10.0,10.0,66,3.61 +69181,91.0,10.0,9.0,10.0,9.0,9.0,10.0,19,1.13 +11941,,,,,,,,1,0.05 +25500,100.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.75 +13386,96.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.65 +61565,75.0,7.0,8.0,8.0,9.0,9.0,7.0,4,0.21 +62200,90.0,9.0,9.0,9.0,9.0,10.0,9.0,8,0.47 +63631,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.42 +45565,94.0,10.0,9.0,10.0,10.0,10.0,10.0,10,1.84 +66361,100.0,8.0,10.0,8.0,4.0,10.0,10.0,1,0.06 +34371,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.19 +4727,,,,,,,,0, +75928,86.0,9.0,9.0,10.0,10.0,10.0,8.0,34,1.76 +37416,99.0,10.0,10.0,10.0,10.0,9.0,10.0,30,1.6 +14172,96.0,10.0,9.0,10.0,10.0,10.0,10.0,78,4.24 +75466,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.64 +22481,95.0,10.0,9.0,9.0,10.0,9.0,9.0,25,1.31 +67072,,,,,,,,0, +58487,91.0,9.0,9.0,9.0,10.0,9.0,9.0,14,0.74 +1535,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.32 +70347,98.0,10.0,10.0,10.0,10.0,10.0,10.0,91,5.14 +16181,99.0,10.0,10.0,10.0,10.0,9.0,10.0,17,0.95 +22386,86.0,9.0,9.0,9.0,9.0,9.0,9.0,87,4.85 +22674,92.0,10.0,10.0,9.0,9.0,9.0,10.0,23,1.25 +60507,92.0,10.0,9.0,10.0,10.0,10.0,9.0,17,1.83 +37213,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.12 +46058,87.0,9.0,9.0,10.0,9.0,9.0,10.0,14,0.91 +26714,100.0,10.0,10.0,10.0,10.0,10.0,7.0,4,0.21 +44845,92.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.35 +55065,97.0,10.0,10.0,10.0,10.0,10.0,10.0,19,1.01 +9622,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.3 +31937,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.19 +41411,90.0,10.0,9.0,9.0,10.0,9.0,9.0,64,3.52 +11641,86.0,9.0,9.0,9.0,9.0,9.0,9.0,8,0.46 +45849,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.57 +42349,87.0,9.0,8.0,9.0,9.0,9.0,8.0,90,4.76 +60130,100.0,10.0,10.0,9.0,10.0,10.0,10.0,5,0.3 +62998,94.0,10.0,9.0,10.0,10.0,9.0,9.0,33,1.79 +14953,93.0,9.0,10.0,10.0,10.0,8.0,9.0,27,1.44 +4157,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.32 +50622,96.0,10.0,10.0,10.0,10.0,9.0,10.0,24,1.27 +27435,,,,,,,,0, +19326,85.0,9.0,9.0,10.0,9.0,9.0,9.0,66,4.47 +50398,70.0,10.0,5.0,10.0,10.0,9.0,8.0,2,0.12 +20762,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.24 +23053,91.0,10.0,9.0,10.0,10.0,8.0,9.0,28,1.51 +40938,99.0,10.0,10.0,10.0,10.0,9.0,9.0,20,1.12 +69467,94.0,10.0,10.0,10.0,10.0,10.0,10.0,28,1.48 +48808,97.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.82 +14694,99.0,10.0,10.0,10.0,10.0,10.0,10.0,39,2.05 +1385,,,,,,,,0, +43540,86.0,9.0,9.0,9.0,10.0,10.0,9.0,7,0.41 +46904,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.82 +45587,90.0,9.0,9.0,10.0,10.0,10.0,9.0,28,1.48 +45707,,,,,,,,0, +22955,91.0,10.0,10.0,10.0,9.0,10.0,9.0,16,0.87 +64052,87.0,9.0,9.0,9.0,9.0,10.0,9.0,32,1.81 +1080,,,,,,,,0, +10703,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +51033,,,,,,,,0, +36569,90.0,9.0,9.0,10.0,10.0,9.0,9.0,37,2.32 +9796,100.0,10.0,10.0,10.0,10.0,10.0,8.0,5,0.29 +73592,81.0,9.0,8.0,10.0,10.0,10.0,8.0,16,0.86 +18641,97.0,10.0,9.0,10.0,10.0,9.0,10.0,13,0.79 +24584,88.0,9.0,9.0,10.0,10.0,10.0,9.0,22,1.46 +75192,,,,,,,,0, +30483,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.38 +43463,96.0,10.0,10.0,10.0,10.0,9.0,10.0,56,3.01 +11221,96.0,9.0,9.0,10.0,10.0,9.0,9.0,15,0.86 +74132,,,,,,,,0, +36662,95.0,9.0,10.0,10.0,10.0,10.0,10.0,8,0.45 +13318,88.0,9.0,9.0,7.0,9.0,10.0,9.0,12,0.64 +76767,85.0,8.0,8.0,8.0,7.0,10.0,8.0,13,0.71 +22114,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +5950,,,,,,,,0, +42404,93.0,9.0,10.0,10.0,9.0,10.0,10.0,8,0.57 +50721,97.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.7 +30261,96.0,10.0,8.0,10.0,10.0,10.0,10.0,6,0.32 +20946,99.0,10.0,10.0,10.0,10.0,9.0,10.0,14,0.8 +59168,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.33 +75684,89.0,9.0,9.0,10.0,10.0,10.0,9.0,40,2.18 +74124,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +53627,94.0,10.0,9.0,10.0,10.0,9.0,9.0,28,1.67 +41563,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.21 +36678,95.0,10.0,9.0,10.0,10.0,10.0,9.0,30,1.7 +25037,94.0,10.0,9.0,10.0,10.0,10.0,9.0,44,2.39 +55055,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.26 +13975,95.0,9.0,9.0,9.0,9.0,9.0,9.0,11,0.58 +2580,80.0,8.0,8.0,7.0,7.0,9.0,6.0,3,0.17 +76990,95.0,10.0,10.0,9.0,9.0,10.0,10.0,8,0.44 +71645,,,,,,,,0, +71313,80.0,10.0,6.0,10.0,10.0,10.0,9.0,2,0.11 +45980,91.0,9.0,9.0,9.0,10.0,9.0,9.0,9,0.49 +45322,96.0,10.0,9.0,10.0,10.0,9.0,9.0,23,1.22 +2886,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.39 +25658,,,,,,,,0, +10024,80.0,8.0,9.0,9.0,9.0,9.0,8.0,11,0.61 +4696,,,,,,,,0, +35942,98.0,10.0,9.0,10.0,10.0,10.0,10.0,11,0.59 +10671,100.0,10.0,9.0,10.0,10.0,9.0,10.0,5,0.27 +44962,100.0,10.0,10.0,9.0,10.0,10.0,10.0,3,0.27 +11766,100.0,10.0,10.0,,,,,1,0.05 +52714,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.05 +9592,100.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.39 +65466,92.0,9.0,9.0,10.0,10.0,9.0,10.0,12,0.66 +29691,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.63 +64062,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.72 +2525,,,,,,,,0, +3264,97.0,10.0,10.0,10.0,10.0,10.0,10.0,121,6.58 +12028,95.0,10.0,10.0,9.0,10.0,9.0,9.0,8,0.63 +40518,91.0,10.0,9.0,10.0,10.0,10.0,9.0,29,1.55 +59368,,,,,,,,0, +15595,,,,,,,,0, +22233,90.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.37 +14402,97.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.53 +27063,,,,,,,,0, +1653,95.0,10.0,10.0,10.0,10.0,10.0,9.0,110,5.9 +34936,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.39 +62025,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.17 +11870,93.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.33 +68910,98.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.61 +37845,70.0,4.0,8.0,7.0,7.0,8.0,8.0,3,0.17 +40768,,,,,,,,0, +20424,100.0,10.0,10.0,10.0,9.0,9.0,10.0,3,0.2 +13085,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,6.36 +56690,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.36 +55356,91.0,9.0,10.0,10.0,10.0,9.0,10.0,20,1.54 +52464,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.19 +74876,,,,,,,,3,0.18 +70460,99.0,10.0,10.0,10.0,10.0,10.0,10.0,32,1.72 +30165,96.0,10.0,9.0,10.0,10.0,10.0,9.0,11,0.61 +2550,93.0,9.0,10.0,9.0,9.0,10.0,10.0,11,0.6 +57747,80.0,9.0,7.0,10.0,9.0,9.0,8.0,3,0.16 +67506,96.0,10.0,9.0,10.0,10.0,10.0,10.0,31,1.67 +1427,98.0,10.0,10.0,10.0,10.0,10.0,10.0,25,1.34 +67709,83.0,9.0,9.0,9.0,9.0,8.0,8.0,70,3.72 +11573,,,,,,,,0, +42032,98.0,10.0,10.0,10.0,10.0,10.0,10.0,41,2.33 +20474,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.82 +3420,50.0,6.0,6.0,8.0,6.0,7.0,6.0,3,0.16 +62144,90.0,9.0,9.0,10.0,10.0,10.0,9.0,2,0.11 +6898,93.0,9.0,9.0,10.0,10.0,9.0,9.0,3,0.16 +48220,93.0,10.0,7.0,10.0,10.0,10.0,9.0,4,0.23 +68122,90.0,10.0,10.0,9.0,10.0,10.0,10.0,2,0.11 +53907,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.17 +55867,67.0,9.0,7.0,9.0,8.0,10.0,6.0,3,0.17 +66087,96.0,10.0,10.0,10.0,10.0,9.0,10.0,33,1.82 +74893,,,,,,,,0, +11398,,,,,,,,0, +26486,88.0,9.0,9.0,9.0,10.0,10.0,9.0,41,2.23 +55941,87.0,9.0,10.0,9.0,8.0,10.0,9.0,3,0.18 +8863,96.0,10.0,9.0,10.0,10.0,10.0,9.0,11,0.67 +42288,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.05 +29757,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.16 +31434,94.0,10.0,7.0,10.0,10.0,9.0,9.0,7,0.38 +38135,98.0,10.0,10.0,10.0,10.0,10.0,9.0,11,0.63 +65543,96.0,10.0,10.0,10.0,10.0,9.0,10.0,36,2.1 +18652,93.0,10.0,10.0,10.0,10.0,10.0,9.0,9,0.74 +61170,,,,,,,,0, +54575,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.26 +54077,91.0,10.0,8.0,10.0,10.0,10.0,9.0,13,0.7 +61958,75.0,8.0,8.0,10.0,9.0,10.0,7.0,4,0.22 +69109,97.0,10.0,9.0,10.0,10.0,10.0,10.0,42,2.3 +64578,100.0,10.0,10.0,9.0,10.0,9.0,10.0,3,0.17 +56116,76.0,8.0,8.0,9.0,9.0,8.0,8.0,11,0.7 +10499,87.0,9.0,9.0,9.0,10.0,8.0,9.0,6,0.36 +74419,76.0,8.0,8.0,7.0,7.0,9.0,8.0,9,0.48 +61092,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.33 +74957,95.0,10.0,10.0,10.0,10.0,10.0,9.0,58,3.15 +18992,96.0,10.0,10.0,10.0,10.0,8.0,10.0,5,0.27 +53327,,,,,,,,0, +8402,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,1.68 +13893,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,2.16 +69090,98.0,10.0,10.0,10.0,10.0,10.0,10.0,104,5.65 +60764,94.0,10.0,9.0,10.0,10.0,10.0,9.0,42,2.47 +53444,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.56 +4466,87.0,9.0,9.0,9.0,9.0,9.0,9.0,25,1.39 +5350,,,,,,,,0, +46041,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.31 +57958,,,,,,,,0, +75082,40.0,6.0,6.0,4.0,6.0,10.0,6.0,1,0.12 +351,,,,,,,,0, +18133,60.0,5.0,7.0,5.0,5.0,5.0,6.0,4,0.22 +3724,,,,,,,,0, +25835,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +12222,93.0,10.0,10.0,9.0,10.0,10.0,10.0,53,2.84 +60732,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +15504,99.0,10.0,10.0,10.0,10.0,10.0,10.0,31,1.65 +1161,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.5 +16235,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.11 +8650,92.0,10.0,10.0,9.0,9.0,9.0,9.0,50,2.75 +9421,92.0,10.0,10.0,9.0,9.0,10.0,9.0,13,0.73 +30515,,,,,,,,0, +46047,82.0,9.0,10.0,8.0,8.0,8.0,8.0,11,0.6 +63905,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.11 +1370,,,,,,,,0, +45201,100.0,10.0,10.0,10.0,10.0,10.0,10.0,27,1.64 +24820,95.0,10.0,10.0,9.0,10.0,10.0,9.0,60,3.33 +40402,90.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.11 +36514,100.0,10.0,10.0,10.0,10.0,10.0,8.0,2,0.11 +36816,80.0,10.0,8.0,8.0,9.0,10.0,8.0,2,0.16 +11532,97.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.83 +21788,,,,,,,,0, +54992,,,,,,,,0, +72259,100.0,10.0,10.0,9.0,10.0,9.0,10.0,3,0.16 +10216,75.0,8.0,7.0,10.0,10.0,10.0,9.0,4,0.23 +53341,91.0,10.0,9.0,10.0,10.0,10.0,9.0,16,0.9 +37374,87.0,9.0,9.0,9.0,9.0,10.0,8.0,17,0.96 +26405,95.0,10.0,9.0,10.0,10.0,10.0,9.0,42,2.38 +25058,,,,,,,,0, +13304,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.22 +11931,99.0,10.0,10.0,10.0,10.0,10.0,10.0,28,1.54 +18222,94.0,10.0,9.0,10.0,10.0,10.0,9.0,27,1.47 +18219,90.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.22 +11480,94.0,10.0,9.0,10.0,10.0,10.0,9.0,24,1.3 +3473,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.11 +51243,90.0,10.0,9.0,10.0,10.0,10.0,8.0,2,0.13 +29357,93.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.2 +11173,87.0,9.0,9.0,9.0,10.0,8.0,9.0,3,0.19 +45591,91.0,9.0,10.0,10.0,10.0,9.0,9.0,15,0.84 +9238,,,,,,,,0, +8674,96.0,10.0,10.0,10.0,10.0,10.0,10.0,45,2.49 +66464,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.42 +26882,90.0,9.0,9.0,10.0,10.0,8.0,9.0,21,1.2 +14015,95.0,9.0,10.0,10.0,10.0,9.0,10.0,48,2.88 +69466,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.5 +64478,89.0,9.0,8.0,9.0,9.0,9.0,9.0,10,0.57 +24328,97.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.76 +39898,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.25 +39616,97.0,10.0,10.0,10.0,10.0,9.0,10.0,37,2.03 +66116,85.0,9.0,8.0,9.0,9.0,10.0,9.0,46,2.67 +42983,95.0,10.0,9.0,10.0,10.0,10.0,9.0,5,0.31 +32250,100.0,10.0,9.0,10.0,10.0,10.0,10.0,8,0.45 +67515,93.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.18 +33028,100.0,10.0,6.0,10.0,10.0,10.0,10.0,2,0.13 +9829,80.0,8.0,6.0,10.0,10.0,10.0,10.0,1,0.06 +70609,90.0,10.0,9.0,10.0,10.0,8.0,10.0,3,0.16 +54497,100.0,10.0,10.0,10.0,10.0,9.0,10.0,42,2.35 +44749,,,,,,,,0, +53337,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +60038,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.7 +17669,90.0,10.0,9.0,9.0,10.0,10.0,9.0,6,0.34 +61839,89.0,9.0,9.0,10.0,9.0,10.0,9.0,23,1.31 +44467,80.0,9.0,8.0,10.0,10.0,9.0,9.0,3,0.17 +45189,94.0,10.0,10.0,10.0,10.0,10.0,9.0,65,3.51 +5144,96.0,9.0,9.0,9.0,9.0,10.0,9.0,10,1.54 +23615,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.29 +17189,100.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.51 +55044,97.0,10.0,10.0,10.0,10.0,10.0,10.0,23,1.39 +44391,86.0,9.0,7.0,10.0,10.0,10.0,9.0,28,3.73 +24755,95.0,10.0,9.0,10.0,10.0,10.0,10.0,8,0.46 +26407,100.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.23 +54254,96.0,10.0,9.0,10.0,9.0,10.0,9.0,11,0.6 +36112,98.0,10.0,10.0,10.0,10.0,10.0,10.0,34,1.97 +37074,90.0,10.0,8.0,9.0,9.0,9.0,9.0,11,0.61 +66524,96.0,10.0,8.0,10.0,10.0,10.0,10.0,6,0.32 +55561,85.0,9.0,8.0,9.0,9.0,8.0,8.0,23,1.81 +9202,93.0,10.0,9.0,10.0,10.0,10.0,10.0,38,2.06 +15120,90.0,10.0,10.0,9.0,9.0,8.0,8.0,4,0.23 +13597,98.0,10.0,9.0,10.0,10.0,10.0,9.0,19,1.08 +23696,85.0,9.0,9.0,10.0,10.0,9.0,9.0,4,0.26 +8794,88.0,10.0,9.0,10.0,10.0,9.0,9.0,12,0.68 +65623,92.0,10.0,8.0,9.0,9.0,10.0,9.0,7,0.4 +54059,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.28 +58186,96.0,10.0,9.0,10.0,10.0,10.0,10.0,5,0.28 +46440,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.52 +26192,92.0,9.0,9.0,9.0,9.0,9.0,9.0,63,3.66 +31721,96.0,10.0,10.0,10.0,10.0,10.0,10.0,41,2.29 +2539,89.0,10.0,9.0,10.0,10.0,9.0,10.0,17,0.93 +42796,98.0,10.0,10.0,10.0,10.0,9.0,9.0,16,0.93 +30568,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.06 +14519,99.0,10.0,10.0,10.0,10.0,9.0,10.0,16,0.95 +25634,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.28 +54608,100.0,10.0,10.0,8.0,10.0,10.0,8.0,1,0.06 +64744,80.0,10.0,7.0,10.0,10.0,9.0,8.0,2,0.12 +13203,92.0,10.0,9.0,10.0,10.0,9.0,10.0,5,0.28 +14678,96.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.65 +70344,87.0,8.0,8.0,8.0,8.0,10.0,9.0,7,0.39 +36851,99.0,10.0,10.0,10.0,10.0,10.0,10.0,36,2.12 +58234,,,,,,,,0, +40385,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.45 +68081,,,,,,,,0, +6160,,,,,,,,0, +28794,94.0,10.0,10.0,10.0,10.0,8.0,10.0,24,1.37 +5434,90.0,9.0,9.0,10.0,9.0,9.0,9.0,14,0.78 +37505,80.0,9.0,7.0,9.0,9.0,9.0,9.0,4,0.22 +63129,95.0,10.0,10.0,10.0,10.0,9.0,9.0,26,1.45 +26811,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +59808,98.0,10.0,10.0,10.0,10.0,9.0,10.0,26,1.48 +53035,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.55 +52088,96.0,10.0,9.0,10.0,10.0,10.0,10.0,10,0.56 +20854,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.45 +31950,95.0,9.0,9.0,10.0,10.0,9.0,9.0,15,0.92 +11597,99.0,10.0,10.0,10.0,10.0,10.0,10.0,85,5.37 +16557,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.28 +29467,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.61 +12669,,,,,,,,1,0.06 +57757,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.21 +61470,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +10884,,,,,,,,0, +47735,,,,,,,,0, +72970,100.0,10.0,10.0,10.0,10.0,8.0,9.0,3,0.18 +36575,87.0,9.0,8.0,10.0,9.0,10.0,9.0,24,1.52 +71353,93.0,10.0,10.0,10.0,10.0,10.0,9.0,21,1.29 +10971,,,,,,,,0, +6363,94.0,10.0,9.0,9.0,10.0,10.0,9.0,13,0.72 +33743,99.0,10.0,10.0,10.0,10.0,10.0,10.0,35,1.94 +62813,100.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.06 +7923,,,,,,,,0, +14442,92.0,10.0,9.0,9.0,10.0,9.0,9.0,15,0.87 +49216,97.0,10.0,9.0,10.0,10.0,9.0,10.0,48,2.79 +23223,96.0,10.0,9.0,10.0,10.0,10.0,10.0,32,1.83 +69231,97.0,10.0,10.0,10.0,10.0,10.0,10.0,62,3.54 +43973,89.0,9.0,9.0,9.0,9.0,9.0,9.0,32,2.06 +70103,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.17 +59038,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.3 +27208,96.0,10.0,10.0,10.0,10.0,9.0,10.0,18,1.03 +53062,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.35 +35752,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.47 +38200,99.0,10.0,10.0,10.0,10.0,10.0,9.0,19,1.07 +61692,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.46 +66291,86.0,9.0,8.0,9.0,9.0,9.0,9.0,45,3.1 +66947,84.0,8.0,6.0,8.0,9.0,9.0,10.0,6,0.35 +73786,75.0,10.0,8.0,8.0,9.0,9.0,8.0,4,0.34 +76111,91.0,9.0,9.0,10.0,10.0,10.0,9.0,14,0.79 +74263,92.0,9.0,10.0,10.0,10.0,10.0,9.0,13,0.74 +49901,,,,,,,,0, +13662,96.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.51 +32722,80.0,9.0,7.0,10.0,9.0,7.0,8.0,3,0.17 +40565,86.0,9.0,9.0,9.0,9.0,10.0,8.0,19,1.09 +21563,80.0,9.0,8.0,8.0,9.0,9.0,8.0,8,0.44 +64391,96.0,10.0,9.0,10.0,10.0,10.0,9.0,5,0.3 +28840,,,,,,,,0, +39172,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.77 +27066,,,,,,,,0, +50829,90.0,9.0,9.0,10.0,10.0,10.0,10.0,4,0.23 +41839,87.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.28 +28470,,,,,,,,0, +6830,90.0,9.0,8.0,10.0,10.0,8.0,9.0,2,0.11 +31113,97.0,10.0,10.0,10.0,10.0,10.0,10.0,58,3.25 +76642,97.0,10.0,7.0,10.0,10.0,10.0,10.0,6,0.35 +62169,96.0,10.0,9.0,10.0,10.0,10.0,10.0,57,3.2 +31031,90.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.35 +51285,94.0,10.0,10.0,9.0,9.0,9.0,10.0,27,1.54 +62802,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +63435,,,,,,,,0, +65291,77.0,9.0,8.0,8.0,8.0,9.0,7.0,7,0.39 +22209,20.0,2.0,2.0,2.0,2.0,6.0,4.0,1,0.07 +28156,95.0,10.0,10.0,9.0,10.0,9.0,9.0,22,1.26 +33622,100.0,9.0,9.0,10.0,10.0,9.0,10.0,2,0.17 +73141,100.0,10.0,10.0,10.0,10.0,9.0,10.0,15,0.88 +1554,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +12333,,,,,,,,0, +5594,99.0,10.0,10.0,10.0,10.0,10.0,10.0,46,2.55 +28799,98.0,10.0,10.0,10.0,10.0,9.0,10.0,33,1.85 +10993,,,,,,,,0, +51533,86.0,10.0,8.0,10.0,10.0,10.0,9.0,11,0.63 +23830,92.0,9.0,9.0,9.0,10.0,10.0,9.0,15,1.0 +60415,83.0,8.0,7.0,9.0,10.0,9.0,9.0,7,0.39 +22186,96.0,9.0,9.0,10.0,10.0,10.0,10.0,36,2.03 +11759,90.0,10.0,9.0,8.0,10.0,10.0,10.0,3,0.27 +60149,,,,,,,,0, +66578,100.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.19 +40425,,,,,,,,0, +68722,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.12 +33337,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.12 +25274,95.0,10.0,10.0,10.0,10.0,9.0,9.0,22,1.23 +39905,93.0,10.0,9.0,10.0,10.0,9.0,9.0,36,2.09 +15380,,,,,,,,0, +23207,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.23 +49473,,,,,,,,0, +4716,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.71 +17055,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.34 +49881,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,1.46 +53667,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.27 +74603,,,,,,,,0, +45750,,,,,,,,0, +36315,85.0,9.0,8.0,10.0,10.0,8.0,8.0,5,0.29 +47772,85.0,9.0,10.0,10.0,10.0,10.0,9.0,8,0.5 +52385,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.06 +17309,98.0,10.0,9.0,10.0,10.0,10.0,10.0,42,2.34 +25254,,,,,,,,0, +30818,95.0,10.0,10.0,10.0,10.0,10.0,9.0,106,5.93 +37258,80.0,8.0,6.0,7.0,7.0,9.0,8.0,3,0.18 +75383,98.0,10.0,10.0,10.0,10.0,9.0,10.0,118,6.73 +37131,90.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.49 +10998,100.0,10.0,9.0,10.0,10.0,9.0,9.0,6,0.35 +8327,,,,,,,,0, +69819,94.0,10.0,10.0,10.0,10.0,9.0,10.0,21,1.49 +12288,100.0,10.0,9.0,10.0,10.0,9.0,10.0,2,0.12 +57875,94.0,10.0,10.0,10.0,10.0,9.0,9.0,35,2.09 +60516,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.17 +50394,97.0,10.0,10.0,10.0,10.0,10.0,10.0,24,1.34 +29420,94.0,10.0,10.0,9.0,10.0,10.0,9.0,113,6.47 +49750,,,,,,,,1,0.09 +58873,,,,,,,,0, +15421,97.0,10.0,10.0,10.0,10.0,10.0,10.0,45,2.68 +853,80.0,9.0,9.0,9.0,10.0,8.0,9.0,3,0.17 +57432,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.06 +45508,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.86 +23808,99.0,10.0,10.0,10.0,10.0,10.0,10.0,25,1.74 +6127,100.0,10.0,9.0,9.0,10.0,9.0,10.0,2,0.11 +31883,92.0,9.0,9.0,10.0,10.0,9.0,9.0,16,0.93 +31093,97.0,10.0,10.0,9.0,10.0,10.0,9.0,23,1.57 +62501,90.0,10.0,9.0,10.0,9.0,10.0,9.0,4,0.27 +18437,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.45 +10923,96.0,10.0,10.0,10.0,10.0,10.0,10.0,198,11.23 +53567,91.0,10.0,9.0,10.0,10.0,9.0,9.0,68,3.89 +45344,98.0,10.0,10.0,10.0,10.0,9.0,10.0,10,0.56 +8175,100.0,10.0,8.0,8.0,8.0,8.0,10.0,1,0.06 +46046,92.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.95 +21322,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.31 +66192,95.0,10.0,9.0,10.0,10.0,10.0,9.0,11,1.63 +2871,76.0,9.0,9.0,8.0,9.0,8.0,8.0,11,0.97 +25417,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.14 +65220,98.0,10.0,10.0,10.0,10.0,10.0,10.0,45,2.61 +40295,94.0,10.0,9.0,10.0,10.0,10.0,9.0,37,2.12 +35587,100.0,10.0,10.0,10.0,10.0,9.0,10.0,8,0.47 +66438,,,,,,,,2,0.13 +61557,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.34 +1437,80.0,9.0,9.0,7.0,8.0,10.0,7.0,6,0.35 +31887,100.0,10.0,9.0,10.0,10.0,9.0,9.0,4,0.36 +43133,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.25 +70416,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.25 +49165,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +66827,91.0,10.0,9.0,10.0,10.0,10.0,10.0,21,1.2 +73730,100.0,9.0,10.0,10.0,10.0,10.0,10.0,10,0.59 +5920,100.0,10.0,10.0,8.0,10.0,8.0,8.0,1,0.06 +21868,98.0,10.0,10.0,10.0,10.0,10.0,10.0,193,10.95 +37442,90.0,10.0,9.0,10.0,10.0,10.0,9.0,18,1.03 +19380,96.0,10.0,10.0,10.0,10.0,10.0,9.0,26,1.48 +66646,91.0,10.0,8.0,10.0,10.0,9.0,9.0,9,0.51 +26121,,,,,,,,1,0.06 +14457,,,,,,,,1,0.08 +35037,100.0,10.0,10.0,10.0,10.0,9.0,10.0,15,0.89 +33988,93.0,9.0,9.0,10.0,10.0,10.0,10.0,3,0.28 +48657,97.0,10.0,9.0,10.0,10.0,9.0,10.0,7,0.43 +55359,97.0,10.0,10.0,9.0,10.0,9.0,9.0,15,1.06 +27883,,,,,,,,0, +72405,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.13 +22977,100.0,9.0,6.0,10.0,10.0,10.0,10.0,2,0.41 +61164,93.0,9.0,9.0,10.0,10.0,10.0,9.0,6,0.34 +30234,87.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.17 +22863,85.0,9.0,10.0,9.0,9.0,9.0,9.0,4,0.35 +72068,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.39 +74481,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.06 +31890,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.19 +30780,93.0,10.0,9.0,10.0,10.0,10.0,9.0,14,0.82 +33066,93.0,9.0,9.0,10.0,10.0,10.0,10.0,3,0.17 +49621,99.0,10.0,10.0,10.0,10.0,10.0,10.0,28,1.71 +14152,97.0,10.0,9.0,10.0,10.0,10.0,9.0,37,2.12 +37159,96.0,10.0,10.0,10.0,10.0,10.0,9.0,11,0.74 +58860,94.0,9.0,9.0,10.0,10.0,10.0,9.0,44,2.49 +62523,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.31 +32625,93.0,10.0,9.0,10.0,10.0,9.0,9.0,150,8.43 +26119,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.14 +16639,,,,,,,,0, +68191,92.0,10.0,9.0,10.0,10.0,9.0,9.0,75,4.28 +8622,92.0,10.0,9.0,10.0,10.0,9.0,9.0,137,7.77 +3198,89.0,9.0,9.0,10.0,10.0,9.0,9.0,142,8.02 +44191,87.0,9.0,9.0,10.0,10.0,9.0,9.0,110,6.21 +9412,95.0,10.0,9.0,10.0,10.0,9.0,10.0,151,8.6 +66343,91.0,10.0,9.0,10.0,10.0,9.0,9.0,162,9.29 +68491,,,,,,,,1,0.07 +1256,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +69308,100.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.26 +54073,93.0,10.0,9.0,10.0,10.0,9.0,9.0,12,0.73 +58686,80.0,10.0,8.0,8.0,10.0,8.0,10.0,1,0.06 +58470,91.0,10.0,9.0,10.0,10.0,9.0,9.0,138,7.83 +28271,94.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.44 +58897,100.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.28 +70666,83.0,9.0,9.0,9.0,9.0,10.0,9.0,24,1.4 +10206,95.0,10.0,10.0,10.0,10.0,10.0,9.0,83,4.87 +41574,98.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.57 +70007,94.0,10.0,9.0,10.0,10.0,10.0,10.0,21,1.21 +36800,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.52 +33054,,,,,,,,0, +38287,,,,,,,,0, +26876,98.0,10.0,10.0,10.0,10.0,10.0,10.0,23,1.35 +25742,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +46071,,,,,,,,0, +37649,100.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.29 +48825,95.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.23 +14541,82.0,9.0,9.0,9.0,10.0,10.0,9.0,13,0.78 +11474,90.0,10.0,9.0,10.0,10.0,10.0,9.0,43,2.53 +13701,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +49808,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +26325,100.0,10.0,10.0,10.0,10.0,10.0,10.0,121,9.28 +62455,93.0,9.0,8.0,9.0,9.0,10.0,10.0,11,0.67 +38378,80.0,8.0,7.0,9.0,8.0,9.0,9.0,3,0.39 +38437,89.0,9.0,9.0,10.0,10.0,10.0,9.0,37,2.59 +41369,89.0,9.0,10.0,10.0,10.0,10.0,9.0,34,2.3 +8687,86.0,9.0,10.0,9.0,9.0,9.0,9.0,34,2.17 +34245,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.31 +10138,92.0,10.0,10.0,9.0,10.0,9.0,9.0,10,0.58 +14187,86.0,9.0,8.0,10.0,10.0,8.0,9.0,10,0.59 +27017,60.0,6.0,6.0,8.0,6.0,8.0,6.0,1,0.06 +52206,100.0,10.0,8.0,10.0,10.0,8.0,10.0,1,0.06 +57789,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.19 +62546,76.0,8.0,8.0,8.0,8.0,9.0,8.0,9,1.03 +44751,90.0,9.0,9.0,9.0,9.0,9.0,9.0,2,0.12 +15735,90.0,9.0,9.0,10.0,10.0,10.0,9.0,3,0.78 +62076,,,,,,,,0, +34811,50.0,6.0,6.0,9.0,7.0,7.0,7.0,2,0.11 +75118,100.0,10.0,10.0,10.0,10.0,10.0,10.0,29,2.64 +2418,95.0,10.0,10.0,10.0,10.0,10.0,9.0,30,1.81 +1248,90.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.15 +71772,97.0,10.0,10.0,10.0,10.0,10.0,9.0,19,1.11 +5026,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.12 +28548,,,,,,,,0, +10704,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +27052,95.0,9.0,10.0,10.0,10.0,10.0,9.0,21,1.25 +65385,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.55 +13427,100.0,9.0,10.0,10.0,10.0,9.0,10.0,5,0.3 +9915,82.0,8.0,8.0,9.0,9.0,10.0,9.0,13,0.85 +57655,,,,,,,,0, +52144,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.54 +76488,88.0,9.0,9.0,10.0,10.0,9.0,9.0,33,1.91 +10708,,,,,,,,0, +3829,89.0,9.0,10.0,10.0,10.0,10.0,9.0,31,1.83 +37622,,,,,,,,0, +24776,89.0,9.0,9.0,10.0,10.0,9.0,9.0,145,8.27 +7613,73.0,7.0,5.0,6.0,7.0,7.0,7.0,3,0.17 +18264,,,,,,,,0, +5916,92.0,10.0,9.0,10.0,10.0,10.0,9.0,26,1.51 +41462,92.0,10.0,10.0,10.0,10.0,10.0,9.0,18,1.23 +32525,89.0,9.0,9.0,10.0,9.0,9.0,9.0,28,1.65 +148,,,,,,,,0, +11075,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +55037,99.0,10.0,10.0,10.0,10.0,10.0,9.0,20,1.14 +5755,90.0,10.0,9.0,9.0,10.0,10.0,10.0,3,0.17 +64781,80.0,10.0,6.0,10.0,10.0,8.0,10.0,1,0.06 +59647,,,,,,,,0, +44726,98.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.87 +47254,98.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.37 +49193,82.0,9.0,9.0,10.0,10.0,10.0,8.0,26,1.88 +66213,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.48 +48524,96.0,10.0,10.0,10.0,10.0,10.0,9.0,53,3.55 +31470,92.0,9.0,9.0,10.0,9.0,9.0,8.0,5,0.29 +4337,97.0,10.0,10.0,10.0,10.0,10.0,10.0,58,3.37 +7239,,,,,,,,0, +72513,,,,,,,,0, +450,70.0,8.0,7.0,9.0,10.0,10.0,8.0,2,0.11 +24262,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.17 +72752,,,,,,,,1,0.24 +36313,81.0,9.0,8.0,9.0,9.0,9.0,8.0,34,1.94 +64598,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.58 +47805,,,,,,,,0, +18361,95.0,10.0,9.0,10.0,10.0,10.0,9.0,16,0.96 +58783,,,,,,,,1,0.06 +7739,96.0,10.0,10.0,9.0,10.0,10.0,9.0,32,1.98 +43207,97.0,10.0,9.0,10.0,10.0,9.0,10.0,15,0.89 +34447,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.31 +23308,,,,,,,,0, +24196,99.0,10.0,10.0,10.0,10.0,10.0,10.0,56,3.28 +35471,92.0,9.0,9.0,10.0,10.0,10.0,9.0,13,0.84 +58954,97.0,10.0,10.0,10.0,10.0,10.0,10.0,35,2.06 +18309,100.0,9.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +50845,,,,,,,,0, +195,,,,,,,,0, +46738,,,,,,,,0, +22131,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.11 +47807,93.0,10.0,10.0,10.0,10.0,10.0,9.0,15,0.86 +31314,97.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.47 +6872,98.0,10.0,10.0,9.0,10.0,9.0,10.0,8,0.48 +36163,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.06 +71845,80.0,9.0,9.0,9.0,9.0,9.0,8.0,39,2.48 +1314,92.0,9.0,10.0,9.0,9.0,9.0,9.0,14,0.94 +23098,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.24 +27082,,,,,,,,1,0.13 +53855,93.0,9.0,9.0,10.0,9.0,10.0,9.0,15,1.12 +18749,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.12 +13842,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.13 +60085,88.0,9.0,8.0,9.0,9.0,10.0,9.0,10,0.58 +72848,94.0,10.0,9.0,9.0,10.0,10.0,10.0,31,1.76 +14943,97.0,10.0,10.0,9.0,10.0,10.0,9.0,44,2.56 +45623,,,,,,,,0, +74012,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +9244,97.0,10.0,9.0,10.0,10.0,10.0,10.0,43,2.5 +26221,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.31 +70417,94.0,10.0,10.0,10.0,10.0,10.0,10.0,17,1.14 +25684,95.0,10.0,9.0,10.0,10.0,9.0,10.0,30,1.84 +36111,98.0,10.0,10.0,10.0,10.0,9.0,9.0,41,2.49 +59619,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.18 +74039,79.0,8.0,8.0,8.0,8.0,9.0,9.0,19,1.12 +42809,,,,,,,,0, +24935,92.0,10.0,9.0,10.0,10.0,10.0,9.0,31,1.92 +58895,90.0,8.0,9.0,9.0,10.0,8.0,8.0,6,0.54 +33943,100.0,10.0,10.0,10.0,10.0,10.0,10.0,18,1.08 +28056,91.0,10.0,9.0,9.0,10.0,9.0,9.0,21,1.25 +14477,91.0,9.0,10.0,10.0,10.0,10.0,9.0,39,2.61 +10071,,,,,,,,0, +10230,,,,,,,,0, +26773,100.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.81 +17546,,,,,,,,0, +49208,98.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.84 +24635,,,,,,,,0, +10459,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.75 +59414,100.0,10.0,10.0,,10.0,,,2,0.12 +27993,96.0,10.0,10.0,10.0,10.0,10.0,9.0,124,7.25 +47652,,,,,,,,1,0.06 +36588,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +29404,,,,,,,,0, +40596,80.0,10.0,8.0,10.0,10.0,10.0,9.0,5,0.29 +61338,,,,,,,,0, +7960,90.0,9.0,8.0,10.0,10.0,10.0,9.0,3,0.17 +55296,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.3 +37133,70.0,10.0,8.0,2.0,8.0,6.0,6.0,2,0.12 +32184,84.0,9.0,9.0,10.0,9.0,10.0,9.0,38,2.43 +2523,,,,,,,,0, +61087,,,,,,,,0, +2176,90.0,9.0,8.0,10.0,10.0,9.0,9.0,7,0.41 +10804,96.0,10.0,9.0,10.0,10.0,10.0,10.0,39,2.25 +8748,95.0,10.0,9.0,10.0,10.0,10.0,10.0,30,2.0 +64197,,,,,,,,0, +38129,,,,,,,,0, +58385,,,,,,,,0, +48205,,,,,,,,0, +73200,93.0,9.0,9.0,10.0,10.0,10.0,9.0,12,0.71 +4664,98.0,10.0,10.0,10.0,10.0,10.0,10.0,50,2.99 +56835,93.0,10.0,9.0,10.0,10.0,10.0,9.0,25,1.75 +57913,,,,,,,,0, +69965,95.0,10.0,9.0,10.0,10.0,9.0,10.0,43,2.53 +53470,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +72129,100.0,8.0,8.0,10.0,8.0,8.0,10.0,2,0.12 +14509,97.0,10.0,10.0,10.0,10.0,10.0,10.0,49,3.29 +44748,,,,,,,,0, +16129,97.0,10.0,10.0,10.0,10.0,10.0,10.0,20,1.23 +35,,,,,,,,0, +37744,78.0,8.0,7.0,9.0,8.0,9.0,7.0,8,0.48 +35352,98.0,10.0,10.0,10.0,10.0,10.0,9.0,9,0.57 +47055,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.24 +42299,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +36703,83.0,9.0,9.0,10.0,9.0,9.0,9.0,15,1.03 +23017,,,,,,,,0, +35187,93.0,10.0,10.0,10.0,9.0,10.0,10.0,7,0.43 +62537,89.0,10.0,9.0,9.0,9.0,9.0,9.0,29,2.19 +57055,86.0,9.0,9.0,10.0,10.0,10.0,9.0,34,2.34 +41962,92.0,9.0,9.0,10.0,9.0,10.0,9.0,14,0.86 +45709,98.0,10.0,10.0,10.0,10.0,10.0,10.0,17,1.01 +19151,100.0,10.0,10.0,10.0,10.0,10.0,10.0,19,1.3 +24858,96.0,10.0,9.0,10.0,10.0,10.0,10.0,16,0.99 +31032,96.0,10.0,9.0,10.0,9.0,9.0,10.0,15,0.87 +37446,98.0,10.0,9.0,10.0,10.0,10.0,10.0,11,0.65 +60970,84.0,9.0,8.0,10.0,9.0,10.0,9.0,22,1.58 +34123,73.0,8.0,7.0,9.0,9.0,10.0,8.0,6,0.36 +64568,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +67845,40.0,8.0,2.0,10.0,6.0,8.0,4.0,1,0.35 +29169,91.0,9.0,9.0,9.0,10.0,10.0,9.0,11,0.69 +27851,97.0,10.0,8.0,9.0,10.0,10.0,9.0,6,0.4 +75639,40.0,6.0,8.0,10.0,10.0,4.0,4.0,1,0.08 +15574,86.0,9.0,8.0,10.0,9.0,10.0,9.0,24,1.61 +28651,,,,,,,,0, +25402,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +61084,93.0,9.0,9.0,10.0,10.0,10.0,10.0,3,0.18 +2919,,,,,,,,0, +53536,,,,,,,,0, +72953,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.18 +5831,98.0,10.0,9.0,10.0,10.0,10.0,10.0,24,1.52 +42231,,,,,,,,0, +20911,99.0,10.0,10.0,10.0,10.0,9.0,10.0,35,2.27 +58887,97.0,10.0,10.0,9.0,10.0,9.0,10.0,6,0.36 +27309,99.0,10.0,10.0,10.0,10.0,9.0,9.0,44,2.59 +62286,,,,,,,,0, +1334,98.0,10.0,10.0,10.0,10.0,10.0,10.0,30,1.92 +31674,60.0,10.0,4.0,10.0,10.0,10.0,10.0,2,0.12 +27862,,,,,,,,0, +57898,,,,,,,,0, +68509,,,,,,,,0, +61502,94.0,9.0,10.0,10.0,10.0,9.0,10.0,13,0.86 +44678,94.0,10.0,10.0,10.0,10.0,9.0,9.0,17,4.32 +340,89.0,9.0,9.0,9.0,9.0,8.0,9.0,25,1.54 +34790,99.0,10.0,10.0,10.0,10.0,10.0,10.0,52,3.05 +12749,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.24 +1918,,,,,,,,0, +37183,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.26 +13478,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.29 +21198,91.0,10.0,9.0,10.0,10.0,10.0,9.0,68,4.04 +56149,91.0,10.0,9.0,9.0,10.0,9.0,9.0,14,0.87 +72837,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +47821,93.0,9.0,9.0,9.0,9.0,9.0,9.0,3,0.19 +42436,,,,,,,,0, +52696,75.0,8.0,10.0,8.0,8.0,10.0,9.0,5,0.3 +55701,94.0,10.0,9.0,10.0,10.0,10.0,10.0,36,2.25 +51431,,,,,,,,0, +4699,85.0,9.0,9.0,9.0,10.0,9.0,9.0,32,2.27 +13991,96.0,10.0,10.0,10.0,10.0,10.0,9.0,38,2.35 +49217,100.0,10.0,10.0,,10.0,,,1,0.06 +68480,97.0,10.0,10.0,9.0,9.0,9.0,10.0,23,1.36 +14383,,,,,,,,0, +58281,91.0,9.0,8.0,10.0,10.0,9.0,9.0,32,2.03 +20418,77.0,9.0,8.0,10.0,10.0,9.0,9.0,13,0.97 +72121,96.0,10.0,9.0,10.0,10.0,10.0,10.0,5,0.43 +30895,,,,,,,,0, +73638,100.0,10.0,10.0,10.0,10.0,10.0,10.0,36,2.4 +55434,80.0,8.0,8.0,8.0,10.0,8.0,6.0,1,0.06 +22459,87.0,10.0,8.0,8.0,8.0,7.0,8.0,3,0.19 +42151,100.0,10.0,10.0,10.0,10.0,9.0,10.0,42,2.9 +31868,,,,,,,,0, +8169,98.0,10.0,10.0,10.0,10.0,10.0,9.0,22,1.32 +66824,,,,,,,,0, +69716,83.0,9.0,8.0,10.0,9.0,9.0,9.0,8,0.48 +76844,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.46 +63462,,,,,,,,0, +12734,,,,,,,,0, +66025,98.0,10.0,10.0,10.0,10.0,10.0,10.0,24,1.46 +33784,95.0,10.0,9.0,10.0,10.0,9.0,10.0,9,0.52 +71377,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.29 +28967,91.0,10.0,10.0,9.0,9.0,10.0,10.0,21,1.33 +28979,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.12 +43071,,,,,,,,0, +11038,84.0,9.0,8.0,9.0,8.0,9.0,9.0,29,1.73 +61891,100.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.24 +7869,87.0,9.0,9.0,10.0,10.0,10.0,9.0,38,2.24 +10916,93.0,9.0,9.0,9.0,9.0,9.0,9.0,3,0.18 +52831,95.0,10.0,10.0,9.0,10.0,10.0,10.0,34,2.66 +59283,93.0,10.0,9.0,10.0,10.0,9.0,9.0,24,1.46 +30300,99.0,10.0,10.0,10.0,10.0,10.0,10.0,30,1.78 +54993,83.0,9.0,9.0,9.0,10.0,9.0,9.0,37,2.64 +26683,,,,,,,,0, +35012,84.0,9.0,9.0,9.0,10.0,9.0,8.0,32,2.14 +57145,,,,,,,,0, +34159,100.0,10.0,10.0,8.0,6.0,8.0,10.0,1,0.06 +11655,,,,,,,,0, +25534,,,,,,,,0, +58937,95.0,10.0,9.0,10.0,10.0,10.0,10.0,16,0.95 +52601,,,,,,,,1,0.21 +58440,,,,,,,,0, +54771,,,,,,,,0, +28802,94.0,10.0,9.0,10.0,10.0,9.0,9.0,21,1.27 +34088,98.0,10.0,10.0,10.0,10.0,10.0,10.0,144,8.94 +62856,,,,,,,,0, +35170,91.0,9.0,9.0,10.0,10.0,9.0,9.0,7,0.49 +11682,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +22503,90.0,9.0,9.0,10.0,10.0,10.0,9.0,4,0.25 +7168,,,,,,,,0, +25859,97.0,10.0,10.0,10.0,9.0,10.0,10.0,12,0.8 +63196,,,,,,,,0, +33624,,,,,,,,0, +8656,,,,,,,,0, +46129,20.0,,,2.0,2.0,2.0,2.0,1,0.19 +76925,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.12 +5786,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.28 +46272,99.0,10.0,10.0,10.0,10.0,10.0,10.0,34,2.04 +562,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.18 +37729,,,,,,,,0, +40494,98.0,10.0,10.0,10.0,10.0,10.0,10.0,62,3.74 +31065,92.0,10.0,9.0,10.0,10.0,9.0,9.0,13,0.8 +19009,87.0,10.0,7.0,10.0,10.0,9.0,10.0,3,0.18 +71319,92.0,9.0,8.0,10.0,10.0,10.0,9.0,5,0.33 +45573,97.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.62 +69438,,,,,,,,0, +49847,95.0,10.0,9.0,10.0,10.0,10.0,9.0,17,1.04 +65772,88.0,9.0,9.0,9.0,10.0,10.0,9.0,40,2.58 +59685,60.0,10.0,10.0,8.0,10.0,8.0,8.0,1,0.23 +72684,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.13 +30008,,,,,,,,0, +18290,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.24 +72948,,,,,,,,0, +28711,99.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.71 +7522,,,,,,,,0, +23436,91.0,10.0,9.0,10.0,10.0,9.0,9.0,57,3.37 +51397,86.0,9.0,9.0,10.0,9.0,10.0,9.0,10,0.6 +2045,40.0,4.0,2.0,10.0,10.0,6.0,4.0,1,0.06 +60868,95.0,10.0,10.0,10.0,10.0,10.0,9.0,49,2.96 +440,100.0,10.0,10.0,10.0,10.0,10.0,10.0,20,1.2 +3319,90.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.18 +37238,98.0,10.0,9.0,9.0,9.0,10.0,10.0,11,0.69 +58648,,,,,,,,0, +42370,88.0,9.0,9.0,10.0,10.0,9.0,9.0,38,2.34 +6763,95.0,10.0,10.0,9.0,9.0,9.0,10.0,77,5.02 +75929,,,,,,,,0, +23737,,,,,,,,0, +26648,81.0,9.0,8.0,9.0,9.0,10.0,9.0,16,1.0 +62942,,,,,,,,0, +37364,86.0,9.0,9.0,9.0,10.0,9.0,9.0,24,1.72 +50786,73.0,8.0,8.0,9.0,9.0,10.0,8.0,3,0.18 +12846,89.0,9.0,9.0,9.0,10.0,10.0,9.0,15,0.95 +28148,93.0,9.0,10.0,10.0,10.0,9.0,9.0,8,0.49 +19634,93.0,9.0,9.0,10.0,10.0,10.0,9.0,24,1.45 +71261,,,,,,,,0, +15530,96.0,10.0,10.0,9.0,10.0,10.0,10.0,5,0.3 +34231,82.0,9.0,10.0,10.0,10.0,10.0,8.0,13,0.78 +45654,91.0,9.0,9.0,10.0,9.0,10.0,9.0,30,2.17 +12527,84.0,8.0,8.0,10.0,9.0,9.0,9.0,5,0.3 +27493,94.0,10.0,9.0,10.0,10.0,10.0,9.0,10,0.63 +65882,83.0,10.0,9.0,9.0,10.0,8.0,9.0,8,0.65 +14460,100.0,8.0,10.0,,,,,3,0.18 +27375,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.65 +61291,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.1 +15509,92.0,10.0,10.0,9.0,10.0,10.0,9.0,17,1.23 +1371,,,,,,,,0, +22260,87.0,9.0,9.0,9.0,9.0,10.0,9.0,22,1.58 +56749,99.0,10.0,10.0,10.0,10.0,10.0,10.0,80,6.54 +30728,97.0,10.0,10.0,10.0,10.0,9.0,10.0,115,7.57 +21454,100.0,10.0,8.0,10.0,8.0,10.0,8.0,2,0.12 +11200,95.0,10.0,10.0,10.0,10.0,10.0,9.0,32,1.9 +66851,64.0,6.0,5.0,8.0,8.0,9.0,7.0,5,0.32 +52701,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.24 +76829,100.0,9.0,10.0,9.0,10.0,9.0,9.0,3,0.26 +2563,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,0.88 +36301,88.0,9.0,10.0,10.0,10.0,10.0,9.0,21,1.54 +14753,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +58611,,,,,,,,0, +26154,98.0,10.0,10.0,9.0,9.0,10.0,10.0,28,1.9 +9448,95.0,10.0,9.0,10.0,10.0,9.0,9.0,26,1.58 +3109,99.0,10.0,10.0,9.0,10.0,10.0,10.0,18,1.15 +19051,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.49 +47062,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.55 +69359,96.0,10.0,10.0,10.0,10.0,10.0,9.0,11,0.66 +70589,100.0,10.0,10.0,10.0,10.0,9.0,9.0,9,0.57 +69001,97.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.41 +22522,20.0,2.0,2.0,2.0,2.0,6.0,4.0,1,0.07 +128,,,,,,,,0, +49705,100.0,10.0,8.0,8.0,10.0,10.0,10.0,1,0.06 +31254,93.0,9.0,10.0,10.0,10.0,10.0,9.0,38,2.79 +35063,94.0,9.0,9.0,10.0,10.0,10.0,10.0,25,1.84 +67902,,,,,,,,0, +46717,,,,,,,,0, +55825,,,,,,,,0, +44519,87.0,9.0,9.0,10.0,10.0,10.0,9.0,37,2.79 +46131,98.0,10.0,10.0,9.0,10.0,9.0,9.0,20,1.2 +28374,94.0,9.0,9.0,10.0,10.0,10.0,9.0,24,1.83 +63042,,,,,,,,0, +39437,98.0,10.0,10.0,10.0,10.0,10.0,10.0,55,3.28 +3430,97.0,10.0,10.0,10.0,9.0,10.0,10.0,12,0.96 +58694,97.0,10.0,9.0,10.0,10.0,10.0,10.0,30,2.27 +30153,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.33 +13547,,,,,,,,0, +29266,92.0,10.0,9.0,10.0,10.0,10.0,9.0,85,5.2 +39545,90.0,9.0,9.0,10.0,9.0,8.0,9.0,39,2.41 +73676,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.32 +68752,,,,,,,,0, +7141,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +66089,98.0,10.0,9.0,10.0,10.0,10.0,9.0,13,1.0 +56979,99.0,10.0,10.0,10.0,10.0,9.0,10.0,65,4.19 +23343,86.0,9.0,9.0,9.0,9.0,9.0,9.0,32,2.41 +72360,100.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.4 +27728,96.0,10.0,10.0,10.0,10.0,10.0,10.0,25,1.55 +56403,100.0,10.0,8.0,10.0,8.0,10.0,8.0,1,0.23 +62233,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.12 +47845,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.43 +29419,95.0,10.0,9.0,10.0,10.0,10.0,9.0,13,0.77 +42986,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.19 +40200,,,,,,,,0, +38802,98.0,10.0,10.0,10.0,10.0,10.0,10.0,122,7.45 +31276,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.19 +26455,92.0,9.0,9.0,10.0,10.0,9.0,9.0,26,1.59 +31351,92.0,10.0,10.0,10.0,10.0,10.0,10.0,21,1.26 +58684,88.0,9.0,9.0,9.0,9.0,10.0,9.0,19,1.25 +70049,87.0,9.0,9.0,10.0,10.0,9.0,9.0,19,1.46 +10737,,,,,,,,0, +49281,88.0,9.0,9.0,10.0,9.0,9.0,9.0,19,1.45 +26278,,,,,,,,3,0.18 +46005,,,,,,,,0, +74041,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.31 +72240,93.0,9.0,10.0,10.0,10.0,9.0,9.0,12,1.0 +33480,100.0,10.0,8.0,10.0,10.0,10.0,8.0,2,0.35 +23791,96.0,10.0,10.0,9.0,10.0,10.0,9.0,11,0.68 +6287,98.0,10.0,10.0,10.0,10.0,10.0,10.0,35,2.29 +46785,,,,,,,,0, +14105,60.0,8.0,2.0,10.0,10.0,10.0,4.0,2,0.13 +63159,,,,,,,,0, +22005,91.0,9.0,8.0,9.0,10.0,9.0,9.0,42,2.66 +55797,85.0,9.0,9.0,9.0,9.0,9.0,9.0,12,0.94 +44127,93.0,9.0,9.0,10.0,9.0,9.0,9.0,3,0.19 +10270,98.0,9.0,9.0,9.0,9.0,9.0,10.0,9,0.66 +59255,,,,,,,,0, +72092,93.0,10.0,9.0,10.0,9.0,10.0,10.0,3,0.19 +50872,88.0,9.0,9.0,9.0,9.0,9.0,9.0,70,4.21 +24307,94.0,10.0,9.0,10.0,10.0,9.0,9.0,18,1.12 +14236,94.0,9.0,9.0,10.0,10.0,10.0,9.0,34,2.28 +43708,95.0,10.0,9.0,10.0,10.0,10.0,9.0,5,0.32 +73079,83.0,9.0,8.0,9.0,9.0,10.0,8.0,39,2.46 +74503,91.0,10.0,10.0,9.0,10.0,9.0,9.0,9,0.69 +76409,94.0,10.0,10.0,10.0,10.0,9.0,9.0,26,2.05 +63169,100.0,10.0,10.0,10.0,10.0,10.0,8.0,2,0.13 +9669,92.0,10.0,10.0,10.0,10.0,9.0,9.0,20,1.59 +48694,74.0,8.0,8.0,6.0,9.0,10.0,8.0,7,0.62 +38197,,,,,,,,0, +51260,89.0,9.0,9.0,9.0,9.0,9.0,9.0,23,1.83 +12362,,,,,,,,0, +65077,98.0,10.0,10.0,10.0,10.0,10.0,9.0,40,2.44 +41223,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.22 +71601,94.0,9.0,9.0,10.0,10.0,9.0,9.0,18,1.14 +16840,97.0,10.0,10.0,10.0,10.0,10.0,10.0,71,4.41 +76860,98.0,10.0,10.0,10.0,10.0,10.0,9.0,33,2.01 +58066,88.0,9.0,9.0,9.0,9.0,9.0,8.0,6,0.36 +49521,94.0,10.0,9.0,10.0,9.0,9.0,9.0,27,1.66 +23561,86.0,9.0,9.0,10.0,9.0,9.0,9.0,28,1.76 +69081,100.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.25 +38877,93.0,9.0,9.0,9.0,9.0,8.0,9.0,3,0.22 +56654,80.0,10.0,10.0,10.0,10.0,10.0,6.0,1,0.23 +69975,95.0,9.0,9.0,10.0,10.0,9.0,9.0,38,2.29 +24849,96.0,10.0,9.0,10.0,10.0,9.0,10.0,16,1.07 +57658,94.0,10.0,10.0,9.0,9.0,10.0,10.0,24,1.48 +75899,98.0,10.0,10.0,10.0,10.0,10.0,9.0,26,1.6 +51670,90.0,8.0,9.0,6.0,9.0,8.0,10.0,2,0.13 +52064,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.25 +62400,96.0,9.0,8.0,9.0,10.0,9.0,9.0,12,0.75 +53550,73.0,8.0,6.0,9.0,8.0,8.0,7.0,6,0.37 +62365,100.0,10.0,10.0,10.0,10.0,6.0,10.0,1,0.14 +26291,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +66685,,,,,,,,0, +24829,99.0,10.0,10.0,10.0,10.0,10.0,10.0,35,2.36 +65684,,,,,,,,0, +58459,,,,,,,,0, +3810,100.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.25 +69564,97.0,10.0,10.0,10.0,10.0,10.0,10.0,37,2.26 +15245,,,,,,,,0, +9614,20.0,6.0,8.0,2.0,4.0,8.0,8.0,2,0.12 +63434,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.13 +67273,100.0,10.0,10.0,10.0,10.0,10.0,10.0,21,1.35 +44066,20.0,2.0,2.0,2.0,2.0,2.0,2.0,1,0.07 +5037,95.0,10.0,8.0,10.0,10.0,9.0,10.0,13,0.8 +50822,,,,,,,,0, +39921,98.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.7 +33803,40.0,6.0,6.0,5.0,4.0,6.0,5.0,2,0.12 +19911,,,,,,,,1,0.08 +14066,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +54817,,,,,,,,0, +10185,,,,,,,,0, +24302,,,,,,,,0, +1521,84.0,8.0,9.0,10.0,10.0,10.0,8.0,11,0.73 +47115,100.0,10.0,4.0,10.0,10.0,10.0,10.0,2,0.12 +31389,88.0,9.0,10.0,9.0,10.0,10.0,10.0,5,0.33 +22213,,,,,,,,0, +65472,80.0,9.0,9.0,9.0,9.0,10.0,9.0,3,0.18 +23103,94.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.42 +24805,93.0,9.0,9.0,9.0,9.0,10.0,9.0,19,1.29 +39967,,,,,,,,0, +66575,,,,,,,,0, +48514,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.62 +52792,,,,,,,,0, +8775,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.37 +45916,98.0,10.0,10.0,10.0,10.0,10.0,9.0,17,1.1 +62532,99.0,10.0,10.0,10.0,10.0,10.0,10.0,18,2.35 +9611,96.0,10.0,10.0,10.0,10.0,10.0,10.0,17,1.04 +40347,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.07 +8938,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.19 +12664,99.0,10.0,10.0,10.0,10.0,10.0,10.0,31,1.93 +28647,80.0,9.0,8.0,9.0,9.0,10.0,8.0,7,0.44 +26645,94.0,9.0,9.0,9.0,10.0,9.0,9.0,17,1.09 +35770,88.0,9.0,10.0,10.0,10.0,10.0,9.0,18,1.19 +558,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +43914,100.0,10.0,8.0,10.0,10.0,8.0,10.0,1,1.0 +40133,100.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.55 +54038,100.0,10.0,10.0,10.0,10.0,9.0,10.0,8,0.48 +17800,20.0,2.0,2.0,6.0,6.0,6.0,2.0,1,0.06 +74566,,,,,,,,0, +64898,,,,,,,,0, +67478,97.0,10.0,10.0,10.0,10.0,9.0,9.0,6,3.75 +11743,96.0,10.0,10.0,10.0,10.0,9.0,9.0,29,1.88 +68978,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +25320,92.0,10.0,10.0,10.0,10.0,9.0,9.0,31,2.08 +37089,74.0,7.0,7.0,7.0,7.0,8.0,8.0,19,1.21 +43903,80.0,6.0,4.0,10.0,10.0,8.0,8.0,1,0.07 +12863,100.0,10.0,10.0,10.0,10.0,10.0,10.0,80,5.1 +47226,100.0,10.0,9.0,9.0,10.0,10.0,10.0,2,0.18 +73085,97.0,10.0,10.0,10.0,10.0,9.0,9.0,15,0.94 +39570,99.0,10.0,10.0,10.0,10.0,10.0,10.0,34,3.38 +50323,85.0,10.0,7.0,10.0,10.0,9.0,8.0,5,0.32 +3872,90.0,10.0,9.0,9.0,10.0,9.0,10.0,4,0.25 +27216,,,,,,,,0, +11350,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.65 +46779,,,,,,,,0, +26762,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.07 +12294,100.0,10.0,7.0,10.0,9.0,10.0,9.0,3,0.19 +19261,92.0,10.0,10.0,10.0,10.0,10.0,9.0,27,1.75 +20593,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.19 +10693,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.34 +40243,92.0,10.0,9.0,10.0,10.0,10.0,9.0,33,2.18 +21631,,,,,,,,0, +46062,85.0,9.0,10.0,10.0,10.0,10.0,10.0,4,0.26 +28934,,,,,,,,0, +8882,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.23 +44108,,,,,,,,0, +67850,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.19 +51046,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.09 +25886,,,,,,,,0, +12676,,,,,,,,0, +31062,,,,,,,,0, +25686,87.0,9.0,8.0,10.0,10.0,10.0,8.0,9,0.57 +7611,91.0,9.0,9.0,9.0,9.0,9.0,9.0,17,1.12 +54890,,,,,,,,0, +27430,,,,,,,,1,0.11 +14247,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +37646,100.0,10.0,10.0,10.0,10.0,7.0,10.0,3,0.21 +57896,,,,,,,,0, +9665,,,,,,,,1, +42927,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.36 +47242,100.0,9.0,9.0,10.0,10.0,8.0,9.0,2,0.15 +55006,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.16 +11658,,,,,,,,0, +41009,95.0,10.0,10.0,9.0,10.0,10.0,9.0,22,1.52 +74575,,,,,,,,0, +70716,99.0,10.0,10.0,10.0,10.0,9.0,10.0,41,2.56 +50201,,,,,,,,0, +59899,,,,,,,,0, +7029,,,,,,,,0, +74625,,,,,,,,0, +73848,99.0,10.0,10.0,10.0,10.0,10.0,10.0,40,2.73 +22815,96.0,10.0,9.0,10.0,10.0,10.0,9.0,34,2.12 +18692,,,,,,,,0, +37022,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +24576,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.42 +19734,96.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.71 +64542,87.0,10.0,8.0,10.0,10.0,10.0,9.0,11,0.67 +53096,87.0,9.0,9.0,10.0,10.0,9.0,9.0,40,2.71 +25934,60.0,6.0,6.0,10.0,10.0,8.0,6.0,1,0.06 +20033,,,,,,,,0, +9606,84.0,9.0,8.0,10.0,9.0,10.0,8.0,5,0.38 +3917,94.0,10.0,9.0,10.0,9.0,10.0,9.0,14,0.88 +29462,95.0,10.0,10.0,10.0,10.0,10.0,9.0,23,1.51 +67962,90.0,9.0,9.0,9.0,9.0,9.0,9.0,2,0.17 +23777,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +26763,,,,,,,,0, +40545,94.0,10.0,10.0,10.0,10.0,9.0,10.0,29,5.4 +10718,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.19 +55889,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.22 +26386,98.0,10.0,10.0,10.0,10.0,10.0,9.0,29,1.9 +15191,,,,,,,,0, +49573,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.19 +47146,,,,,,,,0, +62257,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.45 +69986,95.0,10.0,10.0,10.0,10.0,9.0,9.0,42,2.61 +51355,90.0,10.0,9.0,10.0,10.0,9.0,10.0,6,0.37 +56087,,,,,,,,0, +41057,60.0,6.0,8.0,8.0,4.0,10.0,6.0,1,0.06 +60709,100.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.31 +49671,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.19 +34616,,,,,,,,0, +62940,85.0,9.0,9.0,9.0,9.0,9.0,9.0,8,0.49 +72140,,,,,,,,0, +62702,98.0,10.0,10.0,10.0,10.0,10.0,10.0,44,2.7 +27226,,,,,,,,0, +64011,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.09 +38498,97.0,10.0,10.0,9.0,10.0,10.0,9.0,12,0.85 +34009,,,,,,,,0, +21667,91.0,9.0,9.0,10.0,10.0,9.0,9.0,8,0.57 +8158,,,,,,,,1,0.07 +63626,,,,,,,,0, +28905,100.0,10.0,10.0,10.0,10.0,10.0,10.0,87,5.58 +53463,,,,,,,,0, +13538,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +30899,90.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.27 +41200,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.26 +35007,95.0,9.0,9.0,10.0,10.0,10.0,9.0,8,0.64 +4882,97.0,10.0,10.0,10.0,10.0,9.0,9.0,8,0.55 +56808,94.0,9.0,9.0,10.0,10.0,10.0,9.0,39,2.45 +23912,,,,,,,,0, +67857,93.0,10.0,9.0,10.0,9.0,10.0,9.0,33,2.2 +76810,99.0,10.0,10.0,10.0,10.0,10.0,10.0,31,2.15 +25471,,,,,,,,0, +10031,94.0,10.0,9.0,10.0,10.0,9.0,9.0,28,1.81 +27690,,,,,,,,0, +70230,100.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.32 +16963,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.62 +16117,96.0,10.0,10.0,10.0,10.0,9.0,9.0,21,1.33 +39865,89.0,9.0,9.0,10.0,10.0,9.0,9.0,15,0.95 +60744,,,,,,,,0, +52078,,,,,,,,0, +57980,100.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.22 +70628,97.0,10.0,10.0,10.0,10.0,9.0,10.0,6,1.13 +26155,96.0,10.0,10.0,10.0,10.0,10.0,10.0,37,2.29 +57245,,,,,,,,0, +24444,99.0,10.0,10.0,10.0,10.0,10.0,10.0,27,3.03 +53548,88.0,9.0,9.0,9.0,10.0,9.0,8.0,13,0.84 +29429,95.0,9.0,9.0,10.0,10.0,9.0,9.0,16,0.99 +49343,99.0,10.0,10.0,10.0,10.0,10.0,10.0,34,3.85 +28966,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.1 +46039,100.0,10.0,9.0,10.0,10.0,9.0,10.0,8,0.53 +3708,,,,,,,,0, +32629,86.0,9.0,9.0,9.0,9.0,9.0,9.0,116,7.12 +72378,,,,,,,,0, +32595,99.0,10.0,10.0,10.0,10.0,10.0,10.0,37,2.32 +5489,90.0,9.0,9.0,10.0,10.0,9.0,9.0,48,2.96 +38139,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +72856,96.0,10.0,9.0,10.0,10.0,9.0,10.0,38,2.33 +10563,,,,,,,,0, +54519,100.0,10.0,10.0,10.0,10.0,10.0,9.0,14,0.94 +57622,97.0,10.0,10.0,9.0,9.0,9.0,9.0,6,0.45 +73510,,,,,,,,0, +7787,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.13 +27558,93.0,9.0,9.0,10.0,10.0,9.0,9.0,153,9.43 +60167,92.0,10.0,10.0,10.0,10.0,10.0,9.0,74,4.67 +65054,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +35910,97.0,10.0,10.0,10.0,10.0,10.0,9.0,90,5.64 +42951,90.0,10.0,8.0,10.0,10.0,10.0,10.0,9,0.56 +10065,97.0,10.0,10.0,10.0,10.0,10.0,9.0,22,1.39 +43990,,,,,,,,1,0.1 +2792,96.0,10.0,10.0,10.0,10.0,10.0,9.0,28,1.78 +37110,100.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.19 +50115,,,,,,,,0, +70991,,,,,,,,0, +6217,76.0,9.0,8.0,9.0,9.0,9.0,8.0,10,0.62 +25663,95.0,10.0,9.0,9.0,10.0,10.0,9.0,14,0.92 +48632,,,,,,,,0, +47285,95.0,10.0,10.0,10.0,10.0,9.0,10.0,32,2.0 +69727,85.0,8.0,8.0,10.0,9.0,9.0,8.0,4,0.29 +11792,93.0,10.0,9.0,10.0,10.0,9.0,9.0,14,1.1 +10348,87.0,9.0,9.0,8.0,9.0,10.0,9.0,4,0.27 +25838,88.0,9.0,9.0,10.0,10.0,10.0,9.0,25,1.63 +63202,,,,,,,,0, +5868,99.0,10.0,10.0,10.0,10.0,10.0,10.0,31,2.03 +16586,92.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.43 +69598,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.35 +63978,,,,,,,,0, +17567,,,,,,,,0, +23860,,,,,,,,0, +40681,96.0,10.0,10.0,9.0,10.0,10.0,9.0,36,2.26 +14323,91.0,9.0,10.0,9.0,10.0,9.0,9.0,21,1.71 +28091,,,,,,,,0, +73032,,,,,,,,0, +29362,80.0,6.0,5.0,9.0,10.0,9.0,8.0,2,0.13 +70458,92.0,10.0,10.0,9.0,10.0,10.0,10.0,6,0.38 +54751,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.06 +26275,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.41 +48208,,,,,,,,0, +45185,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.43 +61132,99.0,10.0,10.0,10.0,10.0,9.0,10.0,57,3.73 +1109,92.0,9.0,9.0,10.0,10.0,9.0,9.0,31,2.05 +22210,,,,,,,,0, +32321,98.0,10.0,10.0,10.0,10.0,9.0,10.0,12,0.78 +52886,98.0,10.0,9.0,10.0,10.0,9.0,9.0,8,0.84 +53566,90.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.13 +6346,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.2 +7258,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.31 +27961,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.06 +74000,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.59 +55540,,,,,,,,0, +35654,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.13 +9418,90.0,9.0,10.0,9.0,9.0,10.0,10.0,2,0.13 +21192,,,,,,,,0, +68348,,,,,,,,0, +69571,,,,,,,,0, +19372,93.0,9.0,9.0,10.0,9.0,8.0,10.0,3,0.21 +8386,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +40450,97.0,10.0,10.0,10.0,10.0,9.0,10.0,22,1.41 +50378,94.0,9.0,9.0,10.0,10.0,9.0,9.0,24,1.63 +73239,90.0,9.0,10.0,9.0,10.0,10.0,9.0,12,0.81 +48100,86.0,9.0,9.0,9.0,9.0,8.0,9.0,22,1.47 +27653,96.0,10.0,10.0,10.0,10.0,9.0,10.0,15,0.98 +50279,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.31 +29065,99.0,10.0,10.0,10.0,10.0,10.0,10.0,42,2.82 +37296,99.0,10.0,10.0,10.0,10.0,10.0,10.0,65,4.32 +25228,84.0,9.0,9.0,10.0,9.0,10.0,8.0,10,0.65 +53329,92.0,10.0,9.0,10.0,10.0,10.0,9.0,25,1.62 +41395,100.0,10.0,10.0,10.0,10.0,9.0,10.0,20,1.3 +9316,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.06 +9843,,,,,,,,0, +5922,97.0,10.0,10.0,10.0,10.0,10.0,9.0,35,2.39 +1374,93.0,10.0,9.0,10.0,10.0,9.0,10.0,6,0.38 +36517,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.32 +10881,97.0,10.0,9.0,10.0,10.0,10.0,10.0,14,0.9 +20715,91.0,9.0,9.0,9.0,10.0,10.0,9.0,54,3.55 +55708,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.06 +52437,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.73 +59270,99.0,10.0,10.0,10.0,10.0,10.0,10.0,32,2.12 +45110,100.0,9.0,10.0,10.0,10.0,10.0,9.0,5,0.6 +47294,70.0,8.0,5.0,7.0,8.0,9.0,9.0,3,0.19 +17634,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.18 +6637,100.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.56 +58274,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.13 +1222,93.0,10.0,9.0,10.0,10.0,9.0,10.0,8,0.59 +74719,93.0,10.0,10.0,10.0,9.0,9.0,9.0,6,0.65 +17666,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.07 +51579,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,0.97 +66513,93.0,10.0,9.0,10.0,10.0,10.0,9.0,31,2.06 +56275,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.07 +28712,87.0,9.0,9.0,10.0,10.0,9.0,9.0,14,1.0 +16276,97.0,10.0,9.0,10.0,10.0,10.0,10.0,13,2.45 +17691,77.0,8.0,9.0,9.0,9.0,8.0,8.0,6,0.4 +14552,93.0,9.0,9.0,10.0,10.0,10.0,9.0,42,2.74 +60879,98.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.89 +53478,84.0,9.0,9.0,9.0,9.0,10.0,8.0,23,1.56 +8997,100.0,8.0,8.0,10.0,10.0,8.0,10.0,1,0.07 +20633,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +6442,,,,,,,,0, +1667,,,,,,,,0, +18690,89.0,10.0,9.0,10.0,10.0,10.0,9.0,32,6.11 +62946,,,,,,,,0, +74234,,,,,,,,0, +3087,,,,,,,,0, +19680,96.0,9.0,10.0,10.0,10.0,9.0,9.0,10,0.64 +6755,,,,,,,,0, +32523,97.0,10.0,10.0,9.0,10.0,10.0,10.0,42,2.78 +41183,,,,,,,,0, +38224,97.0,10.0,8.0,9.0,10.0,9.0,9.0,7,0.48 +71178,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.26 +24359,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +19998,80.0,10.0,6.0,6.0,10.0,8.0,6.0,2,0.18 +46153,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +12103,100.0,10.0,9.0,10.0,10.0,8.0,10.0,6,0.42 +31761,,,,,,,,0, +37763,100.0,10.0,10.0,10.0,10.0,9.0,9.0,7,0.45 +16718,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.13 +39666,90.0,9.0,8.0,9.0,10.0,10.0,9.0,2,0.51 +62364,,,,,,,,0, +46420,,,,,,,,0, +49535,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +43325,94.0,10.0,9.0,10.0,10.0,10.0,9.0,18,1.21 +8776,84.0,8.0,9.0,8.0,10.0,10.0,9.0,10,2.13 +48631,83.0,10.0,10.0,10.0,9.0,9.0,9.0,6,0.45 +62746,,,,,,,,0, +66681,80.0,8.0,8.0,10.0,10.0,10.0,10.0,1,0.16 +34023,94.0,9.0,9.0,10.0,9.0,10.0,9.0,48,3.06 +49248,91.0,10.0,9.0,10.0,10.0,10.0,10.0,22,1.46 +59399,95.0,10.0,10.0,10.0,10.0,9.0,9.0,54,3.65 +9990,99.0,10.0,10.0,10.0,10.0,9.0,10.0,28,1.78 +52778,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.81 +48295,100.0,10.0,10.0,10.0,9.0,10.0,9.0,2,0.14 +55202,94.0,10.0,9.0,10.0,9.0,9.0,10.0,22,1.47 +59357,82.0,8.0,9.0,9.0,9.0,10.0,8.0,13,0.88 +76171,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.2 +3735,,,,,,,,0, +63012,84.0,8.0,9.0,10.0,10.0,9.0,8.0,9,0.82 +1817,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +30042,,,,,,,,1,0.07 +29434,87.0,9.0,7.0,10.0,10.0,9.0,8.0,32,2.18 +71264,40.0,8.0,2.0,6.0,10.0,10.0,4.0,1,0.07 +8626,94.0,10.0,9.0,10.0,10.0,10.0,9.0,13,0.86 +75283,95.0,10.0,9.0,10.0,10.0,10.0,9.0,27,1.75 +14123,91.0,10.0,9.0,9.0,10.0,9.0,9.0,19,1.26 +13440,93.0,9.0,9.0,7.0,9.0,9.0,9.0,4,0.26 +55133,96.0,10.0,9.0,10.0,10.0,9.0,10.0,62,3.96 +32091,,,,,,,,0, +28917,97.0,10.0,8.0,8.0,10.0,10.0,9.0,7,0.83 +44474,,10.0,10.0,10.0,10.0,10.0,10.0,1,0.13 +20987,97.0,10.0,10.0,9.0,10.0,10.0,10.0,21,1.83 +49305,80.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.08 +22125,96.0,10.0,10.0,10.0,10.0,9.0,10.0,23,1.45 +24377,99.0,10.0,10.0,10.0,10.0,10.0,10.0,47,2.96 +66759,,,,,,,,0, +34693,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.14 +47341,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +48610,,,,,,,,0, +22467,,,,,,,,0, +25696,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.79 +62939,45.0,4.0,4.0,10.0,5.0,7.0,5.0,4,0.27 +57613,,,,,,,,0, +51593,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,0.2 +67018,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.55 +47784,,,,,,,,0, +76929,100.0,10.0,8.0,10.0,10.0,10.0,10.0,3,0.22 +26075,94.0,10.0,10.0,10.0,10.0,9.0,10.0,17,1.16 +60270,90.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.41 +23478,90.0,10.0,10.0,8.0,10.0,10.0,10.0,2,0.13 +20550,,,,,,,,0, +16728,96.0,9.0,9.0,10.0,10.0,8.0,9.0,5,0.39 +40932,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.99 +70884,,,,,,,,0, +71770,98.0,10.0,10.0,9.0,10.0,10.0,10.0,8,1.45 +20818,93.0,9.0,10.0,9.0,9.0,10.0,9.0,40,2.55 +14199,90.0,9.0,9.0,9.0,9.0,10.0,9.0,29,1.95 +68922,81.0,9.0,9.0,9.0,8.0,8.0,9.0,14,1.3 +18955,95.0,10.0,9.0,9.0,10.0,10.0,10.0,30,2.01 +12385,91.0,9.0,10.0,9.0,9.0,9.0,9.0,14,0.97 +8900,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.2 +68782,20.0,2.0,10.0,4.0,2.0,2.0,2.0,1,0.07 +49201,96.0,10.0,9.0,10.0,10.0,10.0,9.0,14,0.91 +3763,20.0,,,2.0,2.0,,,1,0.07 +74596,93.0,10.0,10.0,10.0,10.0,10.0,9.0,22,1.45 +24392,86.0,10.0,9.0,9.0,9.0,9.0,9.0,13,1.07 +20639,77.0,8.0,9.0,9.0,7.0,8.0,7.0,7,0.52 +42980,,,,,,,,0, +48123,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.26 +40498,,,,,,,,0, +66543,,,,,,,,0, +39466,,,,,,,,0, +41413,100.0,10.0,9.0,10.0,10.0,10.0,9.0,4,0.26 +56164,99.0,10.0,10.0,10.0,10.0,10.0,10.0,58,3.83 +60394,,,,,,,,0, +33135,92.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.35 +63497,100.0,8.0,8.0,8.0,8.0,8.0,8.0,1,0.07 +52533,,,,,,,,0, +71491,90.0,10.0,10.0,8.0,10.0,10.0,10.0,2,0.14 +43314,,,,,,,,0, +50494,,,,,,,,0, +56965,97.0,10.0,10.0,10.0,10.0,9.0,10.0,18,1.19 +61213,100.0,10.0,10.0,10.0,10.0,10.0,10.0,39,2.5 +32434,96.0,10.0,9.0,10.0,10.0,9.0,10.0,25,1.66 +54072,,,,,,,,1,0.07 +18883,96.0,10.0,9.0,10.0,10.0,10.0,9.0,45,2.99 +36542,,,,,,,,1,0.14 +35234,,,,,,,,0, +4423,94.0,9.0,9.0,10.0,10.0,10.0,9.0,13,0.85 +22933,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.29 +73421,100.0,9.0,9.0,9.0,10.0,10.0,10.0,2,0.13 +36643,84.0,9.0,8.0,9.0,9.0,9.0,8.0,5,0.33 +66581,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,1.09 +70740,96.0,10.0,9.0,9.0,10.0,9.0,9.0,18,1.17 +4842,80.0,9.0,8.0,9.0,9.0,8.0,9.0,91,5.92 +56720,,,,,,,,0, +7841,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.59 +52248,,,,,,,,0, +46194,,,,,,,,0, +43584,,,,,,,,0, +55480,95.0,10.0,10.0,10.0,10.0,10.0,9.0,52,3.36 +21517,80.0,9.0,8.0,9.0,9.0,8.0,9.0,83,5.4 +8268,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.32 +58291,,,,,,,,0, +26760,100.0,9.0,9.0,10.0,9.0,9.0,9.0,2,0.13 +54407,,,,,,,,0, +25964,,,,,,,,0, +55519,,,,,,,,0, +71502,85.0,10.0,8.0,9.0,9.0,10.0,9.0,5,0.36 +34162,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.33 +42702,87.0,10.0,10.0,8.0,9.0,10.0,9.0,25,1.88 +5810,96.0,9.0,9.0,10.0,9.0,10.0,9.0,9,0.6 +67413,82.0,9.0,8.0,9.0,9.0,9.0,8.0,39,2.57 +66196,93.0,10.0,9.0,10.0,10.0,10.0,9.0,17,1.29 +45805,92.0,9.0,9.0,10.0,10.0,10.0,9.0,20,1.31 +50048,,,,,,,,0, +50491,82.0,9.0,9.0,9.0,9.0,10.0,8.0,11,0.83 +56010,93.0,10.0,10.0,9.0,10.0,10.0,9.0,27,2.11 +49805,93.0,10.0,10.0,9.0,10.0,10.0,9.0,35,2.79 +21975,96.0,10.0,10.0,10.0,10.0,9.0,10.0,59,4.32 +25340,,,,,,,,0, +42230,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +54461,,,,,,,,0, +61860,89.0,9.0,9.0,10.0,10.0,9.0,9.0,56,3.73 +28036,95.0,10.0,10.0,10.0,10.0,10.0,10.0,35,2.27 +14832,91.0,9.0,9.0,10.0,10.0,10.0,9.0,28,1.83 +10870,98.0,10.0,10.0,10.0,10.0,10.0,10.0,17,1.18 +6163,100.0,10.0,10.0,10.0,10.0,10.0,8.0,2,0.52 +15002,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +47291,97.0,9.0,10.0,10.0,10.0,9.0,9.0,7,0.47 +12720,94.0,9.0,9.0,9.0,10.0,10.0,10.0,21,1.49 +75790,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.57 +27931,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.42 +32100,90.0,9.0,10.0,10.0,10.0,10.0,9.0,9,0.76 +35702,95.0,10.0,10.0,10.0,10.0,10.0,9.0,21,1.92 +12937,84.0,9.0,7.0,10.0,10.0,9.0,9.0,18,1.94 +36618,90.0,10.0,10.0,9.0,10.0,10.0,9.0,14,1.24 +46737,89.0,9.0,10.0,10.0,10.0,9.0,9.0,11,0.96 +16807,94.0,10.0,10.0,9.0,10.0,10.0,9.0,20,1.69 +57365,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.71 +16088,80.0,9.0,9.0,9.0,9.0,9.0,8.0,15,1.22 +66290,97.0,9.0,10.0,10.0,9.0,9.0,9.0,12,0.97 +23781,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.15 +35877,94.0,10.0,10.0,10.0,10.0,10.0,9.0,44,2.94 +53339,93.0,10.0,10.0,10.0,10.0,10.0,9.0,11,0.95 +20634,98.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.5 +9248,90.0,9.0,9.0,9.0,9.0,10.0,9.0,25,1.64 +15087,,,,,,,,0, +58768,,,,,,,,0, +33039,90.0,9.0,9.0,8.0,10.0,9.0,9.0,4,0.35 +16185,80.0,9.0,8.0,8.0,8.0,10.0,9.0,2,0.13 +3960,80.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.07 +54789,,,,,,,,0, +76718,86.0,9.0,9.0,8.0,10.0,10.0,9.0,14,1.08 +52523,,,,,,,,0, +2719,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.29 +51224,87.0,9.0,9.0,9.0,9.0,9.0,9.0,11,0.77 +42617,100.0,10.0,10.0,10.0,10.0,9.0,10.0,20,1.32 +61978,95.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.34 +59460,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.36 +65292,87.0,9.0,8.0,10.0,10.0,10.0,8.0,11,0.72 +53902,96.0,10.0,8.0,10.0,10.0,9.0,9.0,5,0.33 +28394,,,,,,,,0, +19886,,,,,,,,0, +8833,65.0,7.0,8.0,8.0,7.0,10.0,8.0,4,0.63 +40339,,,,,,,,0, +41329,60.0,7.0,8.0,7.0,5.0,9.0,8.0,5,0.35 +23624,80.0,8.0,10.0,9.0,8.0,10.0,8.0,6,0.45 +59940,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.5 +9361,,,,,,,,1,0.33 +49148,92.0,9.0,9.0,10.0,10.0,10.0,9.0,12,0.82 +65577,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +52284,68.0,7.0,6.0,10.0,10.0,9.0,8.0,6,0.4 +76067,85.0,9.0,8.0,9.0,9.0,9.0,9.0,12,0.99 +20368,91.0,9.0,9.0,10.0,9.0,10.0,8.0,14,0.98 +64867,98.0,9.0,9.0,10.0,10.0,9.0,10.0,14,0.97 +1664,,,,,,,,0, +1149,,,,,,,,0, +16772,,,,,,,,0, +33679,96.0,9.0,10.0,10.0,10.0,9.0,10.0,11,0.73 +32488,97.0,10.0,10.0,10.0,10.0,9.0,10.0,30,1.97 +42611,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.82 +18259,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.2 +26035,89.0,9.0,9.0,10.0,10.0,10.0,9.0,18,1.24 +6884,100.0,10.0,10.0,10.0,10.0,10.0,10.0,23,1.53 +46754,,,,,,,,0, +40995,,,,,,,,1,0.07 +14122,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.59 +54688,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.17 +22090,82.0,9.0,8.0,9.0,9.0,9.0,9.0,40,2.67 +33174,85.0,9.0,9.0,9.0,9.0,9.0,9.0,48,3.27 +16625,86.0,9.0,9.0,10.0,9.0,9.0,9.0,31,2.06 +72445,85.0,9.0,9.0,9.0,9.0,8.0,9.0,34,2.3 +30063,86.0,9.0,9.0,9.0,10.0,9.0,9.0,26,1.78 +3098,94.0,10.0,10.0,10.0,10.0,9.0,10.0,32,2.11 +5961,95.0,10.0,10.0,10.0,10.0,9.0,10.0,16,1.05 +48559,93.0,10.0,9.0,10.0,10.0,10.0,10.0,8,0.54 +29731,89.0,9.0,9.0,9.0,10.0,8.0,9.0,9,0.68 +27748,84.0,9.0,9.0,9.0,9.0,10.0,9.0,21,1.38 +16612,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.13 +32359,93.0,10.0,9.0,10.0,10.0,10.0,9.0,6,0.39 +46679,95.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.56 +71394,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.22 +76533,84.0,9.0,9.0,9.0,9.0,8.0,8.0,20,1.92 +65379,97.0,10.0,10.0,10.0,10.0,10.0,10.0,29,1.94 +25251,,,,,,,,1,0.08 +63239,,,,,,,,0, +64655,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.4 +56930,95.0,10.0,10.0,9.0,10.0,10.0,9.0,16,1.12 +20728,,,,,,,,0, +11556,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.49 +31753,,,,,,,,0, +12751,95.0,10.0,10.0,10.0,10.0,10.0,10.0,29,2.25 +49767,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.28 +66295,80.0,9.0,8.0,9.0,9.0,9.0,8.0,8,0.66 +37700,93.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.68 +2362,95.0,10.0,10.0,10.0,10.0,10.0,9.0,52,3.5 +58796,100.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.71 +23960,,,,,,,,0, +7065,,,,,,,,1,0.17 +42652,94.0,10.0,10.0,10.0,10.0,10.0,9.0,13,3.05 +61249,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.21 +55753,87.0,9.0,9.0,10.0,10.0,9.0,9.0,3,0.2 +10494,,,,,,,,0, +19773,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +49827,,,,,,,,0, +38839,,,,,,,,0, +44621,80.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.15 +67966,100.0,10.0,10.0,9.0,10.0,10.0,10.0,25,1.7 +66466,96.0,10.0,10.0,8.0,9.0,9.0,10.0,5,0.35 +31772,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,0.79 +21213,95.0,10.0,9.0,10.0,10.0,10.0,10.0,17,1.35 +71696,96.0,10.0,10.0,10.0,10.0,10.0,9.0,85,5.93 +31863,94.0,10.0,9.0,10.0,10.0,9.0,9.0,10,0.66 +26307,90.0,8.0,9.0,9.0,9.0,10.0,9.0,7,0.5 +77068,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.6 +74731,,,,,,,,0, +45442,90.0,9.0,10.0,10.0,9.0,9.0,9.0,21,1.71 +37247,94.0,10.0,10.0,9.0,10.0,10.0,9.0,7,0.54 +45706,97.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.95 +26609,85.0,10.0,9.0,9.0,10.0,10.0,9.0,5,0.35 +7354,97.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.43 +75475,100.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.27 +27626,,,,,,,,0, +43138,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.26 +29666,85.0,10.0,9.0,6.0,8.0,10.0,9.0,17,1.17 +43011,100.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.55 +16351,82.0,9.0,8.0,9.0,9.0,9.0,9.0,17,1.23 +61536,88.0,10.0,8.0,10.0,10.0,9.0,10.0,6,0.41 +46266,96.0,10.0,9.0,10.0,10.0,10.0,10.0,32,2.13 +31947,88.0,9.0,9.0,9.0,10.0,9.0,9.0,15,1.04 +56009,96.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.4 +11683,90.0,10.0,8.0,10.0,10.0,10.0,9.0,8,0.55 +66514,99.0,10.0,10.0,10.0,10.0,9.0,10.0,18,1.2 +39838,89.0,10.0,9.0,10.0,10.0,10.0,9.0,8,0.54 +13552,,,,,,,,0, +10673,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.67 +68617,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.34 +18589,97.0,10.0,10.0,10.0,10.0,9.0,10.0,58,4.08 +28158,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.48 +15691,,,,,,,,0, +14231,,,,,,,,0, +64617,98.0,9.0,10.0,9.0,10.0,9.0,9.0,9,0.78 +1779,95.0,10.0,9.0,10.0,10.0,9.0,10.0,52,5.49 +27331,,,,,,,,0, +76674,95.0,9.0,10.0,9.0,10.0,9.0,9.0,4,0.35 +10545,92.0,10.0,10.0,10.0,10.0,9.0,9.0,45,3.05 +50581,94.0,10.0,9.0,10.0,10.0,9.0,10.0,25,1.76 +68923,,,,,,,,0, +39580,89.0,10.0,10.0,10.0,10.0,9.0,9.0,33,2.22 +36563,100.0,10.0,8.0,10.0,10.0,8.0,10.0,1,0.13 +61127,90.0,10.0,10.0,10.0,10.0,10.0,9.0,10,0.72 +7433,,,,,,,,0, +34274,,,,,,,,0, +54967,98.0,10.0,9.0,10.0,10.0,9.0,9.0,24,1.64 +18909,97.0,10.0,10.0,10.0,10.0,9.0,10.0,80,5.43 +13923,94.0,10.0,10.0,10.0,10.0,10.0,10.0,14,0.95 +40010,93.0,10.0,10.0,9.0,9.0,9.0,9.0,3,0.21 +17014,97.0,10.0,10.0,10.0,10.0,9.0,10.0,28,1.91 +62918,95.0,10.0,9.0,10.0,10.0,9.0,9.0,39,2.63 +28026,84.0,9.0,8.0,10.0,10.0,9.0,9.0,5,0.34 +19049,,,,,,,,0, +56933,,,,,,,,1,0.07 +53680,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.83 +25456,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.79 +69843,90.0,10.0,9.0,10.0,8.0,10.0,9.0,2,0.39 +34973,95.0,9.0,10.0,9.0,9.0,10.0,9.0,11,0.75 +64457,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,2.93 +29062,91.0,10.0,10.0,10.0,10.0,9.0,9.0,25,1.67 +465,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +33008,100.0,10.0,10.0,10.0,10.0,9.0,9.0,6,0.41 +55259,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.07 +47760,,,,,,,,0, +10027,,,,,,,,0, +58415,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +13240,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.54 +31362,99.0,10.0,10.0,10.0,10.0,10.0,10.0,25,1.75 +38486,98.0,10.0,9.0,10.0,10.0,10.0,10.0,9,1.06 +52091,80.0,6.0,6.0,6.0,7.0,7.0,6.0,2,0.15 +74336,90.0,10.0,9.0,10.0,10.0,10.0,9.0,26,1.89 +71687,84.0,9.0,9.0,8.0,9.0,9.0,8.0,14,0.97 +15464,,,,,,,,0, +58345,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,1.1 +2015,91.0,9.0,9.0,10.0,10.0,9.0,9.0,30,2.11 +20319,94.0,10.0,10.0,10.0,10.0,9.0,9.0,50,3.46 +26688,50.0,6.0,5.0,9.0,9.0,6.0,5.0,2,0.14 +23509,,,,,,,,0, +69690,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.1 +60952,85.0,9.0,10.0,10.0,10.0,9.0,9.0,4,0.28 +38694,85.0,9.0,9.0,9.0,10.0,9.0,9.0,11,0.78 +19783,88.0,9.0,9.0,9.0,9.0,9.0,9.0,8,0.55 +51998,,,,,,,,0, +6459,97.0,10.0,9.0,9.0,10.0,10.0,9.0,18,1.3 +47314,81.0,8.0,8.0,9.0,9.0,8.0,8.0,16,1.1 +74049,96.0,10.0,10.0,10.0,10.0,10.0,10.0,45,3.29 +50976,91.0,9.0,9.0,10.0,10.0,10.0,10.0,23,1.67 +63541,87.0,9.0,8.0,10.0,10.0,9.0,9.0,40,2.7 +6024,,,,,,,,0, +59063,,,,,,,,0, +19848,,,,,,,,0, +50441,80.0,8.0,8.0,8.0,8.0,8.0,8.0,1,0.18 +54294,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.24 +46969,80.0,8.0,8.0,10.0,10.0,10.0,7.0,2,0.14 +21888,20.0,2.0,10.0,8.0,2.0,6.0,2.0,1,0.38 +63790,,,,,,,,0, +39618,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.13 +11208,,,,,,,,0, +17651,93.0,9.0,10.0,10.0,10.0,10.0,10.0,3,0.21 +34333,,,,,,,,0, +39800,60.0,8.0,7.0,8.0,9.0,8.0,6.0,2,0.18 +10383,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.09 +24069,90.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.16 +18469,96.0,10.0,10.0,10.0,9.0,9.0,10.0,33,2.32 +53486,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.79 +2183,93.0,10.0,10.0,10.0,10.0,9.0,9.0,45,3.07 +54907,90.0,10.0,7.0,10.0,10.0,10.0,9.0,7,0.48 +30990,98.0,10.0,10.0,10.0,10.0,10.0,10.0,59,4.06 +12490,97.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.03 +50046,100.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.7 +50804,92.0,10.0,9.0,10.0,9.0,10.0,10.0,5,0.34 +63121,100.0,10.0,10.0,10.0,10.0,10.0,10.0,30,2.17 +7447,,,,,,,,0, +34592,96.0,10.0,9.0,10.0,10.0,10.0,10.0,33,2.3 +49814,80.0,10.0,8.0,10.0,10.0,6.0,8.0,1,0.08 +56016,98.0,10.0,10.0,10.0,10.0,9.0,10.0,18,1.23 +57869,97.0,10.0,10.0,10.0,10.0,9.0,10.0,31,2.32 +62240,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +51147,,,,,,,,0, +14817,90.0,9.0,10.0,9.0,9.0,10.0,9.0,29,2.01 +61266,96.0,10.0,10.0,10.0,10.0,9.0,10.0,42,2.9 +2000,80.0,8.0,6.0,9.0,8.0,7.0,7.0,4,0.33 +33713,,,,,,,,0, +34033,99.0,10.0,10.0,10.0,10.0,9.0,10.0,29,1.97 +46263,98.0,10.0,10.0,10.0,10.0,10.0,9.0,13,0.94 +12777,89.0,9.0,9.0,9.0,9.0,9.0,9.0,14,1.06 +42640,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.52 +65520,93.0,9.0,9.0,9.0,10.0,9.0,9.0,17,1.21 +12922,90.0,9.0,10.0,9.0,9.0,9.0,8.0,15,1.06 +36425,90.0,10.0,10.0,9.0,10.0,10.0,10.0,2,0.14 +20981,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.26 +56390,100.0,10.0,10.0,10.0,10.0,9.0,10.0,16,1.15 +20916,85.0,8.0,8.0,9.0,9.0,8.0,9.0,4,0.54 +73783,,,,,,,,0, +40576,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.4 +45360,,,,,,,,0, +73255,94.0,10.0,10.0,10.0,10.0,10.0,9.0,32,2.25 +32149,,,,,,,,0, +38231,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.08 +15968,,,,,,,,0, +59085,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.66 +13748,96.0,10.0,10.0,10.0,10.0,9.0,9.0,17,1.46 +68625,95.0,9.0,9.0,10.0,10.0,10.0,9.0,15,1.11 +36760,92.0,10.0,10.0,10.0,10.0,10.0,9.0,33,2.28 +42625,94.0,10.0,9.0,10.0,10.0,10.0,9.0,21,1.49 +48356,92.0,9.0,10.0,9.0,9.0,9.0,9.0,19,1.32 +27736,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.14 +56676,,,,,,,,0, +41970,99.0,10.0,10.0,10.0,10.0,9.0,10.0,24,1.67 +31855,91.0,9.0,10.0,9.0,9.0,9.0,9.0,44,3.0 +6039,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.53 +21657,88.0,9.0,9.0,8.0,10.0,9.0,9.0,19,1.61 +19011,97.0,10.0,10.0,10.0,9.0,10.0,10.0,30,2.33 +29616,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +71413,95.0,9.0,10.0,10.0,9.0,9.0,9.0,15,1.04 +44796,95.0,10.0,8.0,10.0,10.0,9.0,9.0,4,0.27 +71550,,,,,,,,0, +33996,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.21 +39217,97.0,10.0,10.0,10.0,10.0,9.0,10.0,33,2.39 +60379,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +43868,93.0,9.0,9.0,9.0,9.0,9.0,9.0,4,0.4 +67996,75.0,8.0,8.0,9.0,8.0,6.0,7.0,4,0.38 +14419,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +49175,91.0,9.0,9.0,9.0,9.0,9.0,9.0,34,2.42 +31272,,,,,,,,0, +42225,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,1.73 +17013,97.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.89 +11985,,,,,,,,1, +9574,100.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.21 +8265,88.0,9.0,9.0,9.0,9.0,9.0,9.0,16,1.2 +7564,93.0,10.0,9.0,10.0,9.0,9.0,10.0,12,0.92 +76352,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.72 +1991,91.0,9.0,9.0,10.0,10.0,9.0,8.0,7,0.55 +12168,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.0 +45150,88.0,9.0,9.0,9.0,9.0,9.0,9.0,22,1.51 +13268,97.0,10.0,10.0,10.0,10.0,10.0,10.0,79,5.42 +2853,96.0,10.0,10.0,10.0,10.0,9.0,9.0,9,0.69 +23193,,,,,,,,0, +25546,97.0,10.0,9.0,9.0,10.0,9.0,9.0,6,0.42 +44413,98.0,10.0,10.0,10.0,10.0,9.0,10.0,11,3.06 +45470,93.0,9.0,8.0,10.0,9.0,10.0,9.0,6,0.47 +42580,,,,,,,,0, +21729,93.0,10.0,10.0,10.0,10.0,9.0,9.0,20,1.42 +37042,85.0,9.0,9.0,9.0,10.0,8.0,9.0,4,0.3 +35950,97.0,10.0,10.0,10.0,10.0,9.0,10.0,14,1.04 +66158,87.0,9.0,10.0,9.0,9.0,10.0,9.0,25,1.92 +25385,95.0,9.0,10.0,10.0,10.0,9.0,9.0,16,1.34 +62314,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.09 +76113,91.0,9.0,9.0,10.0,9.0,10.0,9.0,20,1.42 +60433,,,,,,,,0, +21281,91.0,9.0,9.0,10.0,10.0,10.0,9.0,36,2.67 +12002,98.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.77 +74036,100.0,10.0,10.0,10.0,10.0,8.0,9.0,3,0.26 +4239,91.0,10.0,9.0,9.0,9.0,9.0,10.0,9,0.79 +21548,100.0,10.0,9.0,10.0,10.0,10.0,10.0,11,0.82 +15670,,,,,,,,0, +73808,98.0,10.0,10.0,10.0,10.0,10.0,10.0,96,6.64 +42592,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.28 +34546,100.0,10.0,10.0,8.0,10.0,9.0,10.0,2,0.17 +50120,80.0,8.0,9.0,7.0,9.0,9.0,8.0,4,0.33 +6047,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.22 +71341,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.14 +75352,83.0,9.0,10.0,9.0,8.0,10.0,8.0,7,0.61 +20186,98.0,10.0,10.0,10.0,10.0,9.0,10.0,13,0.91 +49103,,,,,,,,1,0.07 +62062,90.0,10.0,9.0,10.0,10.0,8.0,10.0,2,0.14 +37007,80.0,10.0,8.0,9.0,9.0,9.0,9.0,20,1.49 +60502,88.0,9.0,8.0,9.0,10.0,9.0,9.0,10,0.72 +62352,90.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.28 +34359,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +9002,,,,,,,,0, +67756,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.49 +68842,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.41 +4312,87.0,9.0,7.0,9.0,9.0,8.0,9.0,3,0.23 +6086,100.0,10.0,10.0,10.0,10.0,6.0,8.0,1,0.08 +70891,91.0,10.0,9.0,9.0,9.0,9.0,9.0,17,1.29 +50359,90.0,9.0,8.0,9.0,10.0,10.0,9.0,20,1.43 +60612,,,,,,,,0, +26538,93.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.45 +72962,100.0,10.0,10.0,10.0,10.0,10.0,10.0,75,5.47 +35672,,,,,,,,1,0.07 +3647,89.0,9.0,9.0,9.0,9.0,9.0,9.0,15,1.04 +40147,80.0,9.0,10.0,10.0,9.0,10.0,9.0,4,0.28 +40850,99.0,10.0,10.0,10.0,10.0,10.0,10.0,39,2.79 +6020,,,,,,,,0, +58889,,,,,,,,0, +29348,,,,,,,,0, +70264,92.0,9.0,9.0,10.0,10.0,10.0,9.0,18,1.42 +28232,97.0,10.0,10.0,10.0,10.0,9.0,10.0,47,3.37 +6681,95.0,10.0,10.0,10.0,10.0,10.0,9.0,12,0.9 +10244,90.0,9.0,8.0,10.0,10.0,10.0,10.0,4,0.3 +73392,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +50382,95.0,10.0,10.0,10.0,10.0,9.0,10.0,45,3.22 +65206,80.0,8.0,8.0,8.0,8.0,10.0,6.0,1,0.08 +62619,98.0,10.0,10.0,10.0,10.0,10.0,10.0,45,3.33 +15985,89.0,9.0,9.0,10.0,9.0,10.0,9.0,25,1.79 +43869,98.0,10.0,9.0,10.0,9.0,10.0,10.0,14,0.99 +69027,78.0,9.0,9.0,9.0,7.0,9.0,8.0,11,0.83 +619,93.0,10.0,10.0,10.0,9.0,10.0,9.0,9,0.76 +16562,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.07 +27744,96.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.43 +7915,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +61775,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.19 +30481,100.0,9.0,9.0,10.0,10.0,10.0,9.0,3,0.36 +38292,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +17343,100.0,10.0,10.0,10.0,10.0,10.0,10.0,20,1.43 +64522,96.0,10.0,10.0,10.0,10.0,10.0,9.0,47,4.21 +44843,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.42 +58428,,,,,,,,0, +39203,90.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.22 +28434,,,,,,,,0, +65808,93.0,9.0,9.0,10.0,10.0,8.0,9.0,19,1.35 +10325,78.0,9.0,9.0,10.0,9.0,10.0,9.0,12,0.87 +51652,89.0,9.0,9.0,9.0,9.0,10.0,9.0,19,1.39 +4108,,,,,,,,0, +38083,,,,,,,,0, +53653,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.5 +59157,100.0,10.0,10.0,10.0,10.0,6.0,10.0,1,0.09 +11781,20.0,8.0,2.0,10.0,10.0,6.0,4.0,1,0.19 +24830,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.62 +39974,40.0,6.0,2.0,10.0,10.0,8.0,4.0,1,0.12 +45981,,,,,,,,0, +3043,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.57 +76346,,,,,,,,0, +25884,97.0,10.0,10.0,9.0,9.0,10.0,9.0,19,1.39 +73621,82.0,9.0,8.0,9.0,10.0,10.0,9.0,21,1.56 +30538,,,,,,,,0, +38370,92.0,10.0,9.0,10.0,10.0,10.0,9.0,18,1.29 +15823,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.18 +45745,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.0 +70246,,,,,,,,0, +35657,,,,,,,,0, +60107,95.0,10.0,9.0,10.0,10.0,8.0,10.0,20,1.54 +6167,,,,,,,,0, +57983,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.15 +65515,90.0,9.0,9.0,10.0,10.0,9.0,10.0,36,2.54 +49304,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +68378,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.29 +40237,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +28219,60.0,6.0,4.0,10.0,8.0,6.0,8.0,1,0.11 +3122,85.0,8.0,10.0,9.0,9.0,10.0,9.0,4,0.4 +59400,73.0,8.0,7.0,9.0,8.0,9.0,7.0,26,1.88 +27303,20.0,2.0,2.0,4.0,2.0,6.0,2.0,1,0.21 +49537,,,,,,,,0, +8858,82.0,9.0,9.0,10.0,9.0,10.0,8.0,83,5.89 +66368,,,,,,,,0, +46797,98.0,10.0,10.0,10.0,10.0,10.0,10.0,19,1.52 +36857,,,,,,,,0, +248,,,,,,,,0, +56154,100.0,10.0,10.0,9.0,10.0,9.0,10.0,8,0.59 +63183,,,,,,,,0, +19164,93.0,9.0,8.0,9.0,9.0,10.0,9.0,22,1.61 +19116,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +33956,,,,,,,,0, +38317,91.0,10.0,9.0,10.0,10.0,9.0,9.0,47,3.93 +48165,94.0,10.0,10.0,10.0,10.0,10.0,10.0,38,2.68 +53362,98.0,10.0,10.0,10.0,10.0,10.0,10.0,36,2.79 +47922,78.0,7.0,8.0,8.0,9.0,8.0,8.0,14,1.05 +13944,91.0,9.0,9.0,10.0,10.0,10.0,9.0,31,2.42 +41165,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +57095,96.0,10.0,9.0,9.0,10.0,10.0,9.0,19,1.38 +31315,60.0,6.0,4.0,6.0,6.0,4.0,2.0,1,0.07 +70113,97.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.65 +42335,89.0,10.0,8.0,9.0,10.0,9.0,9.0,12,1.1 +12714,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.08 +2851,,,,,,,,0, +41188,95.0,10.0,10.0,10.0,10.0,10.0,10.0,60,4.97 +27506,93.0,10.0,9.0,10.0,10.0,9.0,10.0,14,1.0 +68117,89.0,9.0,9.0,10.0,10.0,10.0,9.0,32,2.29 +14548,83.0,9.0,9.0,10.0,10.0,10.0,9.0,6,0.43 +14896,77.0,8.0,8.0,9.0,10.0,8.0,9.0,6,0.45 +42390,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.37 +24792,100.0,10.0,10.0,9.0,10.0,9.0,10.0,6,0.43 +55338,,,,,,,,0, +30797,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.41 +8424,100.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.09 +33327,89.0,9.0,8.0,10.0,10.0,9.0,9.0,35,2.59 +14124,100.0,10.0,9.0,10.0,10.0,9.0,10.0,5,1.13 +44544,,,,,,,,1,0.18 +19422,88.0,9.0,8.0,10.0,10.0,9.0,8.0,18,1.29 +61276,93.0,10.0,9.0,10.0,10.0,9.0,10.0,16,1.22 +46586,,,,,,,,0, +73694,99.0,10.0,10.0,10.0,10.0,10.0,10.0,18,1.4 +46927,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,1.08 +1448,89.0,9.0,9.0,10.0,9.0,9.0,9.0,47,3.39 +72549,72.0,8.0,9.0,8.0,8.0,9.0,8.0,5,0.38 +38237,,,,,,,,0, +7454,,,,,,,,0, +64079,90.0,10.0,9.0,10.0,10.0,9.0,9.0,21,1.5 +72595,96.0,10.0,10.0,10.0,10.0,10.0,10.0,95,7.29 +75074,,,,,,,,0, +28212,87.0,9.0,8.0,9.0,9.0,9.0,9.0,28,1.98 +64281,,,,,,,,0, +27568,,,,,,,,0, +64676,100.0,10.0,9.0,10.0,10.0,9.0,9.0,2,0.17 +25212,,,,,,,,0, +58680,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.38 +27580,90.0,10.0,9.0,8.0,9.0,10.0,10.0,4,0.28 +68894,99.0,10.0,10.0,10.0,10.0,10.0,10.0,69,5.46 +36506,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,1.5 +58038,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.19 +50836,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.61 +60102,99.0,10.0,10.0,10.0,10.0,10.0,10.0,38,2.9 +44877,87.0,9.0,9.0,10.0,9.0,9.0,9.0,4,0.29 +37313,,,,,,,,0, +62637,94.0,10.0,10.0,10.0,10.0,9.0,10.0,22,1.61 +48621,85.0,9.0,9.0,9.0,10.0,9.0,9.0,39,2.81 +11977,80.0,9.0,9.0,8.0,8.0,10.0,10.0,7,0.5 +14739,85.0,9.0,8.0,9.0,9.0,9.0,9.0,44,3.32 +29559,85.0,9.0,9.0,9.0,9.0,9.0,9.0,30,2.36 +76050,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.08 +50443,94.0,9.0,10.0,9.0,10.0,9.0,9.0,7,0.53 +43392,,,,,,,,0, +1810,100.0,10.0,10.0,9.0,10.0,10.0,10.0,5,0.37 +17979,89.0,9.0,10.0,8.0,8.0,9.0,9.0,7,0.52 +65821,,,,,,,,0, +27654,91.0,9.0,9.0,10.0,9.0,9.0,9.0,33,2.44 +19630,70.0,10.0,6.0,6.0,10.0,10.0,6.0,2,0.15 +40807,94.0,10.0,9.0,10.0,10.0,9.0,10.0,38,2.81 +45306,,,,,,,,0, +1052,90.0,9.0,9.0,10.0,10.0,9.0,9.0,12,1.04 +13497,80.0,6.0,8.0,10.0,10.0,10.0,8.0,1,0.19 +39187,,,,,,,,3,0.22 +13159,89.0,9.0,9.0,10.0,9.0,10.0,9.0,70,5.12 +14660,85.0,9.0,9.0,9.0,9.0,10.0,8.0,4,0.3 +16038,,,,,,,,0, +60652,95.0,9.0,10.0,10.0,10.0,10.0,9.0,4,0.3 +72581,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.7 +32610,92.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.79 +28064,99.0,10.0,10.0,10.0,10.0,10.0,10.0,52,3.97 +76370,100.0,10.0,10.0,10.0,9.0,9.0,10.0,3,0.34 +47912,97.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.54 +56762,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.42 +76174,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.15 +7371,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.14 +15275,98.0,10.0,9.0,10.0,10.0,10.0,10.0,8,0.65 +18448,94.0,10.0,9.0,10.0,10.0,9.0,10.0,30,2.41 +48648,,,,,,,,0, +44271,91.0,9.0,10.0,9.0,10.0,9.0,9.0,13,1.02 +62877,93.0,10.0,10.0,9.0,10.0,10.0,9.0,8,0.99 +34688,,,,,,,,0, +46594,91.0,9.0,9.0,9.0,10.0,10.0,9.0,28,2.09 +31432,,,,,,,,0, +66398,60.0,6.0,6.0,6.0,6.0,6.0,6.0,2,0.18 +48260,,,,,,,,0, +58654,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.45 +22124,,,,,,,,0, +65721,80.0,8.0,8.0,9.0,9.0,9.0,8.0,17,1.25 +33246,95.0,10.0,10.0,10.0,10.0,9.0,10.0,22,1.65 +67792,97.0,10.0,10.0,10.0,10.0,9.0,9.0,28,2.04 +19490,65.0,8.0,9.0,9.0,9.0,9.0,7.0,4,0.33 +21968,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.17 +10787,83.0,8.0,7.0,10.0,9.0,9.0,9.0,12,1.01 +51052,50.0,5.0,6.0,6.0,6.0,8.0,5.0,2,0.16 +26469,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.09 +17383,20.0,10.0,2.0,10.0,8.0,6.0,4.0,1,0.08 +68000,93.0,9.0,9.0,9.0,9.0,10.0,9.0,3,0.27 +42503,,,,,,,,1,0.09 +37528,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.47 +74139,92.0,9.0,10.0,8.0,9.0,8.0,7.0,5,0.37 +40860,,,,,,,,0, +19733,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +29282,95.0,10.0,10.0,10.0,10.0,10.0,9.0,20,1.52 +28675,94.0,10.0,9.0,10.0,10.0,10.0,10.0,43,3.14 +1295,96.0,10.0,9.0,9.0,10.0,10.0,9.0,19,1.41 +26510,95.0,10.0,10.0,10.0,10.0,10.0,9.0,26,1.89 +53274,97.0,10.0,10.0,10.0,10.0,10.0,10.0,15,1.17 +41122,99.0,10.0,10.0,10.0,10.0,10.0,10.0,22,1.6 +37461,93.0,10.0,10.0,9.0,9.0,9.0,9.0,3,0.22 +31633,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.77 +31385,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.07 +26096,96.0,10.0,10.0,10.0,10.0,9.0,10.0,43,3.61 +1507,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.1 +30327,72.0,7.0,6.0,8.0,8.0,7.0,7.0,5,0.37 +41137,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.23 +61461,90.0,9.0,9.0,10.0,9.0,8.0,9.0,4,0.3 +31974,100.0,10.0,8.0,8.0,10.0,10.0,8.0,2,0.15 +35098,,,,,,,,0, +37278,83.0,8.0,9.0,8.0,9.0,10.0,8.0,13,0.99 +16629,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.15 +42878,92.0,9.0,10.0,9.0,10.0,10.0,9.0,104,7.52 +67552,92.0,9.0,10.0,10.0,10.0,10.0,9.0,5,0.41 +30901,,,,,,,,0, +52729,,,,,,,,0, +63456,92.0,9.0,9.0,10.0,10.0,9.0,10.0,25,1.82 +56345,85.0,9.0,8.0,10.0,9.0,9.0,8.0,11,1.05 +56591,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.44 +55355,96.0,10.0,10.0,10.0,10.0,9.0,9.0,28,2.54 +31755,,,,,,,,0, +32753,98.0,10.0,10.0,10.0,10.0,10.0,10.0,27,2.18 +3347,91.0,9.0,9.0,10.0,10.0,10.0,9.0,11,0.84 +72476,81.0,9.0,8.0,9.0,9.0,9.0,9.0,34,2.58 +38455,86.0,9.0,8.0,10.0,9.0,10.0,9.0,22,1.83 +7158,,,,,,,,0, +53234,73.0,8.0,7.0,9.0,8.0,9.0,8.0,7,0.52 +43640,,,,,,,,0, +8047,98.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.12 +28255,98.0,10.0,10.0,10.0,10.0,10.0,10.0,25,2.25 +4104,,,,,,,,0, +32314,,,,,,,,1,0.15 +13063,,,,,,,,0, +71354,80.0,8.0,10.0,10.0,10.0,10.0,8.0,2,0.15 +66329,,,,,,,,1,0.09 +26065,,,,,,,,0, +18440,,,,,,,,0, +1911,87.0,9.0,9.0,10.0,10.0,9.0,8.0,3,0.24 +29438,80.0,10.0,8.0,10.0,10.0,8.0,10.0,2,0.18 +2858,97.0,10.0,10.0,10.0,10.0,9.0,10.0,18,1.76 +67223,91.0,10.0,9.0,10.0,10.0,10.0,9.0,18,1.36 +75096,,,,,,,,0, +19321,93.0,9.0,9.0,10.0,10.0,10.0,9.0,3,0.22 +28065,91.0,9.0,9.0,9.0,10.0,9.0,9.0,16,1.19 +56304,80.0,9.0,10.0,9.0,10.0,9.0,6.0,3,0.24 +54806,,,,,,,,0, +15730,78.0,9.0,7.0,10.0,10.0,10.0,9.0,12,0.95 +6705,94.0,10.0,10.0,10.0,10.0,10.0,9.0,99,7.23 +49830,90.0,10.0,9.0,9.0,10.0,10.0,9.0,8,0.65 +19875,83.0,9.0,9.0,9.0,9.0,9.0,9.0,14,1.1 +52683,83.0,9.0,8.0,9.0,9.0,9.0,9.0,68,5.06 +56719,81.0,9.0,9.0,10.0,9.0,10.0,9.0,15,1.4 +68234,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,2.19 +67141,95.0,10.0,9.0,10.0,10.0,10.0,10.0,92,7.06 +11134,96.0,9.0,9.0,10.0,10.0,10.0,10.0,10,0.74 +7573,,,,,,,,0, +74375,,,,,,,,1,0.08 +25680,92.0,9.0,9.0,10.0,10.0,9.0,9.0,7,0.51 +28109,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.16 +3084,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.52 +20201,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.46 +13250,100.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.41 +18419,,,,,,,,0, +45165,,,,,,,,0, +59740,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.15 +40607,,,,,,,,0, +59348,,,,,,,,0, +6882,,,,,,,,0, +862,100.0,10.0,10.0,9.0,10.0,10.0,10.0,7,0.52 +57519,99.0,10.0,10.0,10.0,10.0,10.0,10.0,44,3.36 +17539,91.0,9.0,10.0,9.0,9.0,9.0,9.0,13,1.05 +3579,84.0,9.0,9.0,8.0,9.0,10.0,8.0,5,0.5 +16389,87.0,9.0,9.0,9.0,9.0,9.0,9.0,8,0.66 +66762,98.0,10.0,10.0,10.0,10.0,10.0,10.0,23,1.73 +75882,96.0,10.0,9.0,10.0,10.0,10.0,10.0,85,6.28 +51118,,,,,,,,0, +5949,91.0,9.0,9.0,9.0,10.0,10.0,9.0,19,1.41 +49427,,,,,,,,0, +6741,93.0,10.0,9.0,10.0,10.0,9.0,9.0,12,0.99 +46138,,,,,,,,0, +31059,90.0,9.0,9.0,10.0,10.0,10.0,9.0,3,0.64 +33672,91.0,9.0,9.0,9.0,9.0,9.0,9.0,7,0.61 +14805,,,,,,,,0, +7521,90.0,10.0,9.0,9.0,10.0,10.0,9.0,4,0.33 +68890,91.0,9.0,9.0,10.0,10.0,10.0,9.0,87,6.48 +14859,,,,,,,,0, +37014,93.0,9.0,9.0,10.0,9.0,9.0,9.0,17,1.35 +36302,93.0,9.0,9.0,10.0,10.0,10.0,9.0,3,0.25 +46424,,,,,,,,0, +35859,,,,,,,,0, +29553,93.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.16 +50556,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.5 +20212,,,,,,,,0, +63317,90.0,9.0,9.0,9.0,9.0,9.0,9.0,44,3.34 +850,88.0,9.0,9.0,9.0,9.0,9.0,9.0,15,1.14 +44178,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.57 +28871,94.0,10.0,10.0,10.0,10.0,9.0,9.0,7,0.63 +36465,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.09 +57532,85.0,9.0,9.0,8.0,9.0,9.0,9.0,21,1.62 +50345,75.0,8.0,8.0,8.0,8.0,9.0,8.0,4,0.46 +58693,,,,,,,,0, +28140,95.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.7 +71921,90.0,9.0,7.0,10.0,10.0,10.0,9.0,2,0.15 +7710,,,,,,,,0, +59996,,,,,,,,0, +36754,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.48 +68332,83.0,10.0,7.0,10.0,10.0,10.0,9.0,8,0.61 +8280,89.0,9.0,8.0,10.0,10.0,10.0,9.0,15,1.17 +1659,,,,,,,,0, +17536,,,,,,,,1,0.09 +4046,99.0,10.0,10.0,10.0,10.0,10.0,10.0,26,1.98 +40829,96.0,10.0,10.0,10.0,10.0,9.0,10.0,17,1.4 +21124,95.0,9.0,8.0,10.0,9.0,10.0,9.0,4,0.36 +32441,82.0,9.0,8.0,9.0,9.0,8.0,8.0,41,4.27 +23943,100.0,10.0,8.0,10.0,10.0,8.0,6.0,1,0.09 +21898,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +8179,,,,,,,,0, +46185,,,,,,,,1,0.14 +39830,86.0,9.0,9.0,9.0,9.0,10.0,9.0,7,0.57 +40382,90.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.24 +37457,100.0,10.0,8.0,10.0,10.0,10.0,10.0,5,0.38 +36583,,,,,,,,0, +22309,,,,,,,,0, +254,,,,,,,,0, +231,,,,,,,,0, +5931,,,,,,,,0, +17593,,,,,,,,0, +49781,,,,,,,,0, +19311,,,,,,,,0, +24028,,,,,,,,0, +33384,,,,,,,,0, +70144,,,,,,,,0, +3919,,,,,,,,0, +58031,,,,,,,,0, +62444,,,,,,,,0, +48528,,,,,,,,0, +72336,,,,,,,,0, +60341,,,,,,,,0, +23575,,,,,,,,0, +9367,,,,,,,,0, +48527,100.0,10.0,9.0,9.0,10.0,10.0,10.0,9,0.69 +60482,,,,,,,,0, +62896,,,,,,,,0, +75991,,,,,,,,0, +23215,,,,,,,,0, +46247,,,,,,,,0, +59947,,,,,,,,0, +37302,,,,,,,,0, +55574,,,,,,,,0, +11347,,,,,,,,0, +40670,,,,,,,,0, +42256,,,,,,,,0, +10195,,,,,,,,0, +14388,,,,,,,,0, +63650,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.28 +58664,,,,,,,,0, +13389,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.18 +1093,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.14 +72104,,,,,,,,0, +37961,,,,,,,,0, +68887,,,,,,,,0, +42186,,,,,,,,0, +16857,,,,,,,,0, +34521,,,,,,,,0, +28030,,,,,,,,0, +74379,,,,,,,,0, +18712,,,,,,,,0, +41806,,,,,,,,0, +46110,,,,,,,,0, +68230,,,,,,,,0, +46581,,,,,,,,0, +13259,,,,,,,,0, +37244,,,,,,,,0, +25086,,,,,,,,0, +12891,,,,,,,,0, +36607,,,,,,,,0, +34680,,,,,,,,0, +61437,,,,,,,,0, +63333,,,,,,,,0, +18848,,,,,,,,0, +6574,,,,,,,,0, +70149,,,,,,,,0, +6267,,,,,,,,0, +62976,,,,,,,,0, +47166,,,,,,,,0, +50413,,,,,,,,0, +74422,,,,,,,,0, +18277,,,,,,,,0, +25270,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.25 +66887,,,,,,,,0, +18513,,,,,,,,0, +68549,,,,,,,,0, +52411,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.41 +19838,,,,,,,,0, +52937,,,,,,,,0, +64431,,,,,,,,0, +25743,,,,,,,,0, +52324,,,,,,,,0, +53408,,,,,,,,0, +76927,,,,,,,,0, +34854,,,,,,,,0, +64624,,,,,,,,0, +44929,,,,,,,,0, +17410,,,,,,,,0, +46346,,,,,,,,0, +329,,,,,,,,0, +11996,,,,,,,,0, +36970,,,,,,,,0, +25328,,,,,,,,0, +21545,,,,,,,,0, +67020,,,,,,,,0, +67991,,,,,,,,0, +11509,,,,,,,,0, +22751,,,,,,,,0, +9955,,,,,,,,0, +56369,,,,,,,,0, +10486,,,,,,,,0, +17555,,,,,,,,0, +8847,93.0,9.0,9.0,10.0,10.0,10.0,9.0,4,0.8 +1401,,,,,,,,0, +62835,,,,,,,,0, +32814,,,,,,,,0, +29943,,,,,,,,0, +13077,,,,,,,,0, +75075,,,,,,,,0, +28241,,,,,,,,0, +60412,,,,,,,,0, +19548,,,,,,,,0, +75019,,,,,,,,0, +50340,,,,,,,,0, +75034,,,,,,,,0, +44363,,,,,,,,0, +21780,,,,,,,,0, +43065,,,,,,,,0, +52811,,,,,,,,0, +50145,,,,,,,,0, +43949,,,,,,,,0, +22946,,,,,,,,0, +19343,,,,,,,,0, +70454,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.19 +15606,,,,,,,,0, +70337,,,,,,,,0, +11348,,,,,,,,0, +36324,,,,,,,,0, +39362,,,,,,,,0, +2008,,,,,,,,0, +8304,,,,,,,,0, +62471,,,,,,,,0, +34968,,,,,,,,0, +65866,,,,,,,,0, +9709,,,,,,,,0, +76036,,,,,,,,0, +8881,,,,,,,,0, +26400,94.0,10.0,10.0,10.0,10.0,10.0,10.0,19,1.47 +70098,,,,,,,,0, +18367,,,,,,,,0, +26631,,,,,,,,0, +59041,,,,,,,,0, +9027,84.0,9.0,9.0,10.0,8.0,9.0,9.0,11,0.85 +72865,89.0,10.0,9.0,9.0,10.0,9.0,9.0,39,2.96 +42403,100.0,10.0,10.0,10.0,8.0,10.0,8.0,1,0.19 +15402,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.31 +68270,,,,,,,,0, +33534,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.15 +45798,,,,,,,,0, +28797,99.0,10.0,10.0,10.0,10.0,10.0,9.0,14,1.19 +6917,95.0,10.0,10.0,9.0,10.0,9.0,10.0,5,0.41 +20434,93.0,10.0,10.0,10.0,10.0,9.0,9.0,23,1.84 +26967,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,0.94 +44778,85.0,10.0,10.0,10.0,10.0,8.0,10.0,4,0.36 +70796,93.0,10.0,9.0,9.0,10.0,10.0,9.0,16,1.32 +2861,90.0,8.0,7.0,10.0,10.0,10.0,9.0,2,0.17 +60541,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +64325,82.0,9.0,8.0,8.0,9.0,9.0,9.0,39,3.0 +15483,88.0,9.0,10.0,10.0,9.0,9.0,8.0,17,1.35 +51858,,,,,,,,0, +71063,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.27 +18825,88.0,9.0,9.0,9.0,9.0,9.0,9.0,19,1.53 +33623,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.16 +33514,91.0,10.0,9.0,10.0,10.0,9.0,9.0,15,1.38 +49629,93.0,9.0,9.0,9.0,9.0,9.0,9.0,20,1.57 +69643,,,,,,,,0, +26514,80.0,9.0,9.0,9.0,9.0,9.0,8.0,6,0.94 +27155,100.0,10.0,10.0,9.0,10.0,10.0,10.0,12,0.96 +74660,84.0,10.0,10.0,10.0,8.0,9.0,8.0,6,0.52 +55041,90.0,10.0,10.0,10.0,10.0,8.0,10.0,4,0.32 +70818,100.0,9.0,10.0,10.0,10.0,9.0,9.0,5,0.41 +29300,94.0,9.0,10.0,10.0,10.0,10.0,9.0,10,0.82 +76952,,,,,,,,0, +62113,90.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.17 +43782,80.0,8.0,8.0,8.0,9.0,9.0,8.0,13,0.99 +36409,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.81 +40111,98.0,10.0,10.0,10.0,10.0,9.0,10.0,29,2.24 +32682,,,,,,,,0, +20260,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.25 +60625,99.0,10.0,10.0,10.0,10.0,10.0,10.0,16,1.24 +75628,94.0,10.0,9.0,9.0,10.0,9.0,10.0,14,1.11 +29185,,,,,,,,0, +18536,87.0,9.0,9.0,9.0,9.0,10.0,8.0,4,0.62 +7106,93.0,9.0,9.0,9.0,9.0,9.0,9.0,3,0.27 +19794,75.0,8.0,9.0,10.0,9.0,10.0,8.0,4,0.53 +44768,93.0,10.0,10.0,10.0,10.0,9.0,9.0,95,7.36 +75996,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.27 +19871,,,,,,,,0, +6265,90.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.71 +21560,98.0,10.0,9.0,8.0,9.0,10.0,9.0,8,0.93 +71758,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.89 +56176,96.0,9.0,9.0,6.0,9.0,9.0,9.0,5,0.41 +17311,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +74112,60.0,10.0,8.0,10.0,8.0,10.0,6.0,1,0.08 +72154,84.0,9.0,9.0,9.0,9.0,10.0,8.0,18,1.48 +20929,90.0,10.0,9.0,10.0,9.0,10.0,9.0,16,1.7 +23268,100.0,9.0,9.0,10.0,10.0,10.0,10.0,4,0.32 +59550,88.0,9.0,8.0,9.0,9.0,10.0,9.0,10,0.83 +37254,86.0,9.0,8.0,9.0,9.0,10.0,8.0,13,1.07 +37383,73.0,8.0,10.0,8.0,7.0,9.0,8.0,6,0.51 +10886,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +7762,,,,,,,,0, +3006,,,,,,,,0, +64118,100.0,9.0,10.0,10.0,10.0,9.0,9.0,2,0.16 +59039,,,,,,,,0, +51142,93.0,9.0,9.0,10.0,10.0,10.0,9.0,6,0.5 +10878,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.23 +66944,95.0,10.0,10.0,8.0,10.0,10.0,9.0,4,0.34 +5124,,,,,,,,1,0.08 +982,97.0,10.0,9.0,10.0,10.0,9.0,10.0,50,4.02 +27953,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,1.01 +1961,,,,,,,,0, +46378,86.0,9.0,9.0,9.0,9.0,10.0,9.0,85,6.61 +52582,100.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.33 +55897,97.0,10.0,10.0,10.0,10.0,9.0,10.0,48,3.81 +18320,100.0,10.0,10.0,10.0,10.0,9.0,10.0,16,1.3 +61135,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.13 +13113,80.0,8.0,7.0,7.0,8.0,10.0,7.0,2,0.19 +55203,87.0,9.0,8.0,10.0,9.0,10.0,8.0,6,0.49 +33568,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.09 +30652,,,,,,,,0, +4541,73.0,9.0,7.0,9.0,9.0,10.0,8.0,9,0.88 +15239,97.0,10.0,9.0,10.0,10.0,10.0,9.0,6,0.48 +63302,98.0,10.0,9.0,10.0,10.0,10.0,10.0,12,0.94 +47334,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.31 +16482,,,,,,,,0, +1294,96.0,10.0,9.0,9.0,10.0,10.0,9.0,10,0.87 +1304,,,,,,,,1,0.29 +20466,92.0,10.0,10.0,10.0,9.0,9.0,9.0,21,1.64 +46362,97.0,10.0,10.0,9.0,10.0,10.0,9.0,13,1.05 +8174,,,,,,,,0, +51668,92.0,9.0,9.0,8.0,9.0,9.0,9.0,5,0.39 +55257,87.0,9.0,9.0,10.0,10.0,9.0,9.0,15,1.17 +1984,,,,,,,,0, +14688,,,,,,,,1,0.13 +37281,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.69 +12978,100.0,10.0,10.0,10.0,10.0,8.0,9.0,6,0.58 +20470,82.0,8.0,8.0,9.0,10.0,9.0,9.0,13,1.02 +36981,97.0,10.0,10.0,10.0,10.0,10.0,10.0,27,2.93 +29806,92.0,10.0,9.0,10.0,10.0,9.0,9.0,18,1.42 +32633,83.0,9.0,9.0,10.0,9.0,9.0,8.0,23,1.79 +60672,90.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.16 +49611,79.0,9.0,8.0,9.0,9.0,8.0,8.0,20,1.57 +46623,,,,,,,,0, +13678,95.0,10.0,9.0,10.0,10.0,10.0,10.0,15,1.34 +72841,89.0,10.0,9.0,9.0,9.0,10.0,9.0,7,0.57 +4313,,,,,,,,0, +57521,92.0,10.0,9.0,9.0,10.0,9.0,9.0,26,2.04 +62729,94.0,10.0,10.0,10.0,10.0,9.0,9.0,14,1.1 +31958,,,,,,,,0, +38294,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.09 +13858,85.0,8.0,9.0,9.0,8.0,9.0,9.0,9,0.79 +25522,,,,,,,,0, +26173,96.0,10.0,9.0,10.0,10.0,10.0,9.0,71,5.61 +24834,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.42 +52326,95.0,10.0,9.0,10.0,10.0,10.0,10.0,11,2.02 +25392,99.0,10.0,10.0,10.0,10.0,10.0,10.0,119,9.15 +1360,90.0,9.0,9.0,10.0,10.0,9.0,10.0,22,1.86 +6394,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.26 +76842,86.0,9.0,9.0,9.0,10.0,9.0,9.0,60,4.77 +27249,85.0,9.0,8.0,9.0,9.0,9.0,9.0,69,5.55 +12538,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.26 +18804,94.0,9.0,9.0,9.0,10.0,9.0,9.0,7,0.6 +27384,,,,,,,,1,0.09 +72373,,,,,,,,0, +74149,87.0,9.0,7.0,8.0,8.0,9.0,9.0,6,0.63 +41904,98.0,10.0,10.0,10.0,10.0,10.0,9.0,20,1.57 +50021,40.0,6.0,2.0,4.0,8.0,8.0,6.0,3,0.23 +36452,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.64 +64801,,,,,,,,0, +50100,,,,,,,,0, +49983,,,,,,,,0, +22603,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.23 +50809,100.0,10.0,10.0,10.0,10.0,10.0,9.0,9,0.71 +28201,85.0,9.0,9.0,9.0,9.0,9.0,8.0,56,4.36 +20747,90.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.24 +45618,40.0,6.0,2.0,8.0,6.0,6.0,2.0,1,1.0 +13980,100.0,10.0,10.0,10.0,10.0,8.0,8.0,2,0.16 +25521,,,,,,,,0, +42246,97.0,10.0,10.0,10.0,10.0,10.0,9.0,24,2.52 +74445,98.0,10.0,10.0,10.0,10.0,10.0,10.0,37,3.44 +22754,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.56 +4534,100.0,10.0,10.0,10.0,10.0,9.0,9.0,6,0.52 +35676,95.0,10.0,10.0,9.0,10.0,9.0,10.0,4,0.33 +3002,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +3638,100.0,10.0,10.0,9.0,10.0,10.0,10.0,4,0.35 +41496,95.0,10.0,9.0,10.0,10.0,10.0,10.0,23,1.86 +69906,98.0,10.0,10.0,10.0,10.0,9.0,9.0,20,1.76 +22857,,,,,,,,1,0.11 +14927,90.0,9.0,10.0,10.0,10.0,10.0,10.0,8,0.64 +61230,100.0,10.0,10.0,9.0,9.0,10.0,10.0,4,0.46 +74652,96.0,10.0,10.0,10.0,10.0,10.0,10.0,62,5.41 +28791,,,,,,,,1,0.08 +64756,90.0,9.0,9.0,9.0,9.0,7.0,9.0,4,0.35 +31672,100.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.31 +468,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.42 +18986,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.18 +55950,,,,,,,,0, +12053,93.0,10.0,9.0,10.0,10.0,10.0,9.0,17,1.41 +41524,100.0,10.0,10.0,6.0,8.0,10.0,10.0,1,0.1 +63630,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.24 +28205,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.31 +48608,,,,,,,,0, +55081,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.09 +30906,97.0,10.0,9.0,10.0,10.0,9.0,10.0,18,1.72 +66421,93.0,9.0,9.0,9.0,9.0,9.0,9.0,3,0.75 +22610,,,,,,,,0, +42087,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.67 +24497,,,,,,,,2,0.16 +52694,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.59 +12674,83.0,9.0,8.0,9.0,9.0,9.0,9.0,24,1.88 +45112,,,,,,,,0, +21861,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.18 +31425,,,,,,,,0, +49398,91.0,10.0,9.0,9.0,10.0,10.0,9.0,28,3.77 +12224,92.0,10.0,9.0,9.0,10.0,10.0,9.0,31,3.39 +42130,,,,,,,,0, +42834,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.16 +16951,91.0,9.0,10.0,9.0,9.0,10.0,9.0,19,1.6 +55454,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.09 +13637,,,,,,,,0, +72150,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.33 +57650,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.94 +26434,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.12 +2497,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.71 +73362,98.0,10.0,10.0,10.0,10.0,10.0,9.0,20,1.78 +9398,83.0,9.0,9.0,10.0,9.0,10.0,8.0,7,0.56 +76554,98.0,10.0,10.0,10.0,10.0,9.0,10.0,8,0.83 +54481,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +57714,96.0,10.0,9.0,10.0,10.0,9.0,10.0,19,3.29 +49699,,,,,,,,1,0.11 +59924,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.18 +32186,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.15 +27754,,,,,,,,0, +43864,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.36 +4250,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.37 +76793,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.32 +67538,,,,,,,,1,0.08 +9247,70.0,7.0,9.0,6.0,7.0,8.0,8.0,3,0.25 +13598,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.3 +4943,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +32052,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +76564,,,,,,,,0, +39746,,,,,,,,0, +46533,87.0,9.0,9.0,8.0,9.0,10.0,9.0,6,2.4 +53132,89.0,9.0,10.0,9.0,9.0,8.0,9.0,15,1.74 +20619,88.0,10.0,8.0,10.0,10.0,9.0,9.0,5,0.44 +62982,94.0,10.0,9.0,9.0,9.0,9.0,9.0,26,2.64 +59181,,,,,,,,0, +3485,,,,,,,,0, +1531,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +3366,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.08 +52994,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.7 +38751,100.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.17 +42500,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.63 +22768,87.0,8.0,9.0,9.0,9.0,7.0,9.0,4,0.32 +45215,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.09 +31050,99.0,10.0,10.0,10.0,10.0,9.0,9.0,21,1.85 +69534,,,,,,,,0, +19457,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.67 +2142,97.0,10.0,10.0,10.0,9.0,9.0,10.0,7,0.58 +10526,90.0,9.0,9.0,10.0,10.0,9.0,9.0,10,0.82 +39246,,,,,,,,0, +29211,,,,,,,,0, +12910,92.0,9.0,10.0,9.0,9.0,9.0,8.0,12,0.98 +60885,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.17 +59964,95.0,10.0,9.0,10.0,10.0,9.0,9.0,11,0.89 +47290,87.0,8.0,9.0,10.0,10.0,10.0,9.0,6,0.53 +63312,,,,,,,,0, +73077,,,,,,,,0, +61599,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.5 +61851,96.0,10.0,9.0,9.0,10.0,8.0,9.0,5,0.54 +32323,90.0,8.0,10.0,8.0,8.0,9.0,9.0,4,0.43 +70368,,,,,,,,0, +52882,81.0,9.0,8.0,9.0,10.0,9.0,9.0,65,5.15 +57313,80.0,9.0,9.0,8.0,10.0,10.0,10.0,2,0.16 +2797,100.0,8.0,8.0,8.0,8.0,8.0,8.0,1,0.08 +56775,96.0,10.0,10.0,10.0,10.0,10.0,8.0,9,0.79 +31557,,,,,,,,0, +71554,80.0,8.0,9.0,10.0,8.0,10.0,9.0,4,0.37 +6117,60.0,8.0,6.0,10.0,10.0,8.0,8.0,1,0.15 +62293,,,,,,,,0, +38777,,,,,,,,0, +32256,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +35972,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.93 +41555,,,,,,,,0, +52451,95.0,10.0,9.0,10.0,10.0,10.0,9.0,12,0.97 +9325,40.0,2.0,2.0,2.0,2.0,6.0,4.0,1,0.08 +48128,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,1.77 +45997,,,,,,,,0, +44607,78.0,9.0,7.0,9.0,9.0,9.0,9.0,86,6.94 +57884,,,,,,,,0, +69290,,,,,,,,0, +13045,93.0,9.0,10.0,10.0,9.0,9.0,9.0,47,3.97 +32125,80.0,7.0,10.0,9.0,9.0,9.0,8.0,3,0.31 +71103,,,,,,,,0, +41730,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.63 +10010,88.0,10.0,10.0,9.0,10.0,10.0,9.0,8,0.69 +20204,80.0,10.0,10.0,10.0,8.0,10.0,10.0,1,0.25 +19205,96.0,10.0,10.0,10.0,10.0,10.0,9.0,34,2.76 +19228,96.0,10.0,10.0,10.0,10.0,10.0,10.0,15,1.29 +28458,,,,,,,,0, +32017,93.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.79 +10733,,,,,,,,0, +73583,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.48 +46601,98.0,10.0,10.0,10.0,10.0,9.0,10.0,36,2.98 +37887,,,,,,,,0, +13569,86.0,9.0,8.0,10.0,10.0,9.0,9.0,14,1.12 +74433,,,,,,,,1,0.08 +3811,72.0,9.0,8.0,9.0,9.0,8.0,8.0,5,0.64 +48102,95.0,10.0,10.0,10.0,10.0,10.0,9.0,11,0.95 +54204,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.95 +63495,68.0,8.0,8.0,8.0,8.0,8.0,7.0,6,0.53 +4448,,,,,,,,0, +8314,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +62774,90.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.17 +8575,,,,,,,,0, +56726,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,1.69 +43992,96.0,9.0,10.0,9.0,10.0,9.0,10.0,6,0.51 +51947,100.0,9.0,9.0,10.0,10.0,9.0,9.0,2,0.18 +66242,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.17 +28780,100.0,9.0,9.0,9.0,9.0,10.0,9.0,2,0.17 +43724,98.0,10.0,9.0,10.0,10.0,10.0,10.0,8,1.29 +6967,,,,,,,,0, +5060,,,,,,,,0, +20235,97.0,10.0,10.0,10.0,10.0,10.0,9.0,14,1.39 +29804,85.0,10.0,10.0,10.0,9.0,10.0,9.0,4,0.33 +62290,93.0,10.0,8.0,10.0,10.0,9.0,9.0,21,1.68 +28134,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.38 +71165,97.0,10.0,10.0,9.0,10.0,10.0,9.0,15,1.24 +42476,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.81 +28386,100.0,10.0,10.0,10.0,10.0,10.0,8.0,2,0.13 +6748,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.4 +31019,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.16 +45945,60.0,6.0,6.0,10.0,10.0,10.0,4.0,1,0.47 +20835,,,,,,,,0, +71357,100.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.38 +16486,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.13 +71322,,,,,,,,0, +48038,,,,,,,,2,0.16 +46337,,,,,,,,0, +54343,,,,,,,,0, +27631,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.16 +36001,90.0,9.0,8.0,6.0,9.0,10.0,10.0,2,0.17 +13379,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.39 +39054,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +54170,97.0,10.0,9.0,10.0,10.0,9.0,9.0,7,1.5 +58777,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.44 +15547,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.33 +70612,97.0,10.0,10.0,9.0,10.0,10.0,9.0,18,1.65 +55739,93.0,10.0,9.0,10.0,10.0,10.0,10.0,13,1.09 +75975,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.09 +55223,,,,,,,,0, +44349,80.0,8.0,8.0,8.0,6.0,8.0,8.0,1,0.52 +72822,,,,,,,,0, +76103,100.0,10.0,9.0,9.0,10.0,9.0,10.0,4,0.34 +26118,90.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.35 +24234,80.0,7.0,8.0,9.0,8.0,10.0,7.0,2,0.2 +7758,,,,,,,,0, +31333,60.0,8.0,6.0,10.0,10.0,10.0,6.0,1,0.61 +51045,,,,,,,,0, +68419,,,,,,,,1,0.08 +16193,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.25 +64359,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,1.03 +37740,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.75 +8530,96.0,10.0,9.0,10.0,10.0,10.0,9.0,5,0.41 +5558,,,,,,,,0, +56052,,,,,,,,0, +44618,91.0,10.0,9.0,10.0,10.0,10.0,9.0,8,0.66 +75813,,,,,,,,0, +31011,,,,,,,,0, +26552,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,0.62 +52812,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.1 +76656,99.0,10.0,10.0,10.0,10.0,10.0,10.0,32,2.87 +17424,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.33 +13308,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.37 +16824,98.0,10.0,10.0,10.0,10.0,10.0,10.0,21,1.73 +51005,97.0,10.0,9.0,10.0,10.0,10.0,9.0,12,1.14 +21309,80.0,8.0,8.0,9.0,8.0,8.0,9.0,7,0.58 +35684,100.0,10.0,10.0,9.0,10.0,10.0,10.0,14,1.16 +23506,100.0,10.0,10.0,10.0,10.0,10.0,10.0,42,3.48 +48814,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,1.0 +53295,93.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.42 +17395,95.0,10.0,10.0,10.0,10.0,10.0,9.0,16,1.3 +15928,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.37 +36629,,,,,,,,0, +61778,100.0,9.0,9.0,9.0,10.0,8.0,9.0,5,0.44 +12185,94.0,10.0,9.0,10.0,10.0,10.0,10.0,13,1.07 +48704,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.08 +38041,90.0,9.0,10.0,10.0,9.0,10.0,9.0,2,0.25 +46154,,,,,,,,0, +45372,83.0,9.0,8.0,9.0,9.0,10.0,9.0,12,0.99 +15111,80.0,9.0,9.0,10.0,9.0,9.0,8.0,5,0.43 +31734,,,,,,,,0, +73521,,,,,,,,0, +7835,97.0,10.0,10.0,10.0,10.0,10.0,10.0,23,5.61 +18902,90.0,10.0,10.0,10.0,10.0,9.0,9.0,21,1.86 +63854,,,,,,,,0, +64363,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,1.17 +62103,,,,,,,,0, +22538,90.0,10.0,9.0,10.0,10.0,9.0,10.0,5,0.44 +56012,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.1 +62377,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,0.82 +45003,93.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.35 +75439,96.0,10.0,10.0,10.0,10.0,10.0,9.0,83,7.3 +51242,,,,,,,,0, +12301,80.0,8.0,10.0,6.0,8.0,8.0,10.0,1,0.14 +63137,93.0,10.0,7.0,10.0,10.0,9.0,9.0,3,0.54 +9264,76.0,8.0,8.0,9.0,9.0,9.0,8.0,22,1.79 +22052,,,,,,,,0, +70267,90.0,10.0,9.0,10.0,10.0,9.0,9.0,12,1.05 +27219,91.0,8.0,8.0,9.0,9.0,9.0,8.0,12,1.02 +35476,,,,,,,,0, +26787,100.0,8.0,8.0,8.0,10.0,8.0,10.0,1,0.24 +28409,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.49 +1373,99.0,10.0,10.0,10.0,10.0,10.0,10.0,25,3.26 +75529,,,,,,,,0, +48653,,,,,,,,1,0.08 +27378,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.41 +52727,100.0,10.0,10.0,10.0,10.0,10.0,9.0,11,0.97 +42941,94.0,9.0,9.0,10.0,10.0,9.0,9.0,14,1.24 +63106,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.18 +32005,,,,,,,,0, +56320,,,,,,,,0, +33707,94.0,10.0,10.0,10.0,10.0,10.0,9.0,8,0.79 +242,,,,,,,,0, +67321,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.1 +4292,,,,,,,,0, +29225,91.0,10.0,9.0,10.0,10.0,9.0,9.0,72,6.3 +47656,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.62 +71697,99.0,10.0,10.0,10.0,10.0,9.0,10.0,57,5.72 +73787,,,,,,,,0, +14232,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.36 +21943,,,,,,,,0, +29227,94.0,10.0,10.0,9.0,10.0,10.0,10.0,54,4.75 +6555,,,,,,,,0, +19999,96.0,10.0,10.0,10.0,10.0,10.0,10.0,17,1.43 +51220,100.0,10.0,9.0,9.0,8.0,10.0,9.0,3,0.4 +57951,92.0,9.0,9.0,10.0,10.0,10.0,9.0,29,3.05 +55368,,,,,,,,0, +5150,90.0,10.0,8.0,10.0,10.0,9.0,10.0,2,0.43 +43309,76.0,9.0,10.0,8.0,7.0,10.0,8.0,8,0.65 +60420,,,,,,,,0, +58202,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +17698,,,,,,,,0, +46122,95.0,8.0,10.0,10.0,10.0,10.0,8.0,4,0.4 +60708,80.0,10.0,10.0,6.0,5.0,10.0,8.0,2,0.23 +14379,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.79 +10305,,,,,,,,0, +67469,94.0,10.0,10.0,10.0,10.0,10.0,9.0,56,5.12 +60820,84.0,9.0,8.0,10.0,9.0,9.0,9.0,9,1.18 +48042,96.0,9.0,9.0,10.0,10.0,10.0,9.0,6,0.5 +27684,,,,,,,,0, +32261,87.0,9.0,8.0,9.0,10.0,9.0,9.0,6,0.73 +333,,,,,,,,1,0.14 +50408,100.0,9.0,9.0,9.0,10.0,9.0,9.0,3,0.51 +23821,,,,,,,,0, +48292,,,,,,,,0, +44289,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +30411,,,,,,,,1,0.08 +25563,87.0,7.0,8.0,9.0,9.0,9.0,8.0,3,0.26 +68384,,,,,,,,0, +34674,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,3.04 +60428,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +17353,87.0,9.0,9.0,9.0,10.0,9.0,9.0,3,0.26 +55413,94.0,10.0,9.0,9.0,10.0,9.0,10.0,29,2.44 +27884,,,,,,,,0, +11436,,,,,,,,0, +4358,95.0,10.0,10.0,10.0,10.0,9.0,9.0,20,1.68 +20532,,,,,,,,1,0.08 +38154,100.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.84 +73985,,,,,,,,1,0.08 +15648,,,,,,,,1,0.15 +54569,91.0,10.0,9.0,10.0,10.0,9.0,10.0,13,1.46 +28816,100.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.61 +70360,100.0,10.0,9.0,10.0,10.0,10.0,10.0,7,1.04 +68905,,,,,,,,0, +76764,,,,,,,,0, +17744,,,,,,,,0, +42092,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.2 +39488,,,,,,,,0, +57060,100.0,10.0,10.0,9.0,10.0,10.0,10.0,5,0.42 +27479,80.0,9.0,9.0,10.0,9.0,9.0,9.0,8,0.71 +4327,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.08 +23140,80.0,6.0,8.0,10.0,6.0,10.0,10.0,1,0.09 +30118,67.0,7.0,7.0,7.0,8.0,9.0,7.0,4,0.5 +17562,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.18 +49359,100.0,9.0,10.0,9.0,10.0,10.0,9.0,4,0.39 +63866,99.0,10.0,10.0,10.0,10.0,10.0,10.0,18,1.52 +36791,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.35 +62649,,,,,,,,0, +72124,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.8 +18092,84.0,8.0,9.0,8.0,8.0,8.0,9.0,5,0.9 +27863,97.0,10.0,10.0,10.0,10.0,10.0,9.0,22,2.2 +43388,97.0,10.0,10.0,10.0,10.0,10.0,9.0,34,3.05 +64888,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,3.53 +22638,,,,,,,,0, +57343,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +43882,,,,,,,,1,0.09 +71460,98.0,10.0,10.0,9.0,10.0,10.0,9.0,9,1.08 +66712,,,,,,,,0, +54572,97.0,10.0,10.0,10.0,10.0,9.0,10.0,14,1.21 +10701,87.0,10.0,10.0,9.0,10.0,8.0,9.0,17,1.51 +71221,,,,,,,,0, +36320,,,,,,,,0, +21607,,,,,,,,0, +41323,,,,,,,,0, +11924,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.24 +29294,95.0,10.0,10.0,10.0,8.0,10.0,10.0,4,0.35 +12919,90.0,9.0,9.0,9.0,9.0,10.0,8.0,14,1.18 +23880,98.0,10.0,9.0,10.0,10.0,9.0,10.0,22,1.94 +53933,90.0,10.0,8.0,10.0,10.0,9.0,9.0,2,0.18 +38345,80.0,10.0,9.0,9.0,9.0,9.0,9.0,3,0.46 +41763,98.0,10.0,10.0,9.0,9.0,10.0,10.0,10,0.99 +27007,,,,,,,,0, +22360,96.0,9.0,9.0,10.0,10.0,9.0,9.0,17,1.5 +73420,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.21 +5100,90.0,9.0,10.0,9.0,10.0,10.0,9.0,3,1.27 +5945,97.0,10.0,9.0,10.0,9.0,10.0,9.0,6,0.53 +25057,78.0,8.0,8.0,8.0,9.0,10.0,9.0,14,1.22 +26116,,,,,,,,1,0.08 +47982,80.0,9.0,9.0,9.0,10.0,10.0,8.0,3,0.26 +3173,94.0,10.0,10.0,10.0,10.0,9.0,10.0,58,4.86 +26264,,,,,,,,0, +21236,,,,,,,,1,0.14 +20224,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.18 +18532,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.3 +33524,80.0,8.0,10.0,10.0,10.0,8.0,8.0,2,0.18 +26944,94.0,9.0,9.0,10.0,10.0,10.0,10.0,16,1.53 +56840,90.0,10.0,8.0,8.0,10.0,10.0,9.0,2,0.18 +3468,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +50313,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.39 +30252,95.0,10.0,9.0,10.0,10.0,10.0,10.0,16,1.34 +39132,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.26 +63519,,,,,,,,0, +55569,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.09 +54889,,,,,,,,0, +17223,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +41671,,,,,,,,0, +56939,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.17 +38340,73.0,9.0,7.0,10.0,9.0,9.0,9.0,9,0.86 +70233,,,,,,,,0, +27248,87.0,10.0,8.0,9.0,9.0,9.0,9.0,12,1.05 +75196,,,,,,,,0, +11459,,,,,,,,0, +69833,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.18 +73886,100.0,10.0,10.0,9.0,10.0,9.0,10.0,4,0.36 +20873,87.0,10.0,8.0,9.0,10.0,10.0,9.0,11,1.05 +15255,91.0,10.0,9.0,10.0,10.0,9.0,9.0,11,1.11 +42228,92.0,9.0,10.0,9.0,10.0,10.0,9.0,12,1.14 +65263,95.0,10.0,9.0,10.0,10.0,9.0,10.0,25,2.49 +10241,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +835,,,,,,,,0, +71503,80.0,9.0,9.0,10.0,10.0,8.0,9.0,2,0.17 +33468,80.0,9.0,9.0,9.0,8.0,8.0,9.0,3,0.26 +29617,80.0,9.0,9.0,9.0,10.0,10.0,9.0,9,0.78 +36567,98.0,10.0,10.0,10.0,10.0,9.0,10.0,49,4.5 +34848,,,,,,,,0, +59490,94.0,10.0,10.0,10.0,10.0,10.0,9.0,22,3.01 +73198,,,,,,,,0, +52176,,,,,,,,0, +73612,,,,,,,,1,0.09 +65857,79.0,9.0,9.0,10.0,9.0,10.0,8.0,18,1.74 +16528,88.0,9.0,9.0,9.0,9.0,10.0,8.0,22,2.36 +55610,90.0,9.0,9.0,9.0,9.0,10.0,9.0,21,1.83 +51336,,,,,,,,0, +48731,93.0,10.0,8.0,10.0,10.0,10.0,9.0,17,1.49 +16768,92.0,9.0,9.0,9.0,9.0,9.0,9.0,19,1.69 +40927,,,,,,,,0, +20856,90.0,9.0,10.0,9.0,9.0,9.0,9.0,20,2.23 +54390,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.49 +26607,93.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.93 +63125,80.0,10.0,7.0,9.0,10.0,10.0,9.0,3,0.29 +40784,87.0,10.0,9.0,10.0,10.0,10.0,10.0,6,0.87 +48837,,,,,,,,1,0.09 +9031,,,,,,,,0, +39648,,,,,,,,0, +40553,100.0,10.0,10.0,10.0,10.0,10.0,10.0,24,4.71 +18885,,,,,,,,0, +23290,90.0,10.0,10.0,10.0,9.0,8.0,10.0,2,0.24 +36547,100.0,10.0,10.0,10.0,10.0,10.0,9.0,11,1.22 +62019,93.0,9.0,10.0,10.0,10.0,9.0,10.0,4,0.34 +28860,95.0,10.0,10.0,10.0,10.0,9.0,9.0,20,2.03 +72320,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.25 +60124,,,,,,,,0, +31696,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.09 +19477,91.0,10.0,9.0,10.0,10.0,10.0,10.0,22,1.96 +36931,80.0,10.0,10.0,8.0,8.0,10.0,6.0,1,0.09 +9445,,,,,,,,0, +45216,80.0,7.0,8.0,7.0,9.0,9.0,9.0,3,0.46 +36648,98.0,10.0,10.0,10.0,10.0,9.0,10.0,12,1.78 +13173,94.0,10.0,9.0,10.0,10.0,10.0,9.0,38,3.34 +66306,93.0,9.0,9.0,10.0,10.0,10.0,10.0,19,1.84 +52348,89.0,9.0,9.0,10.0,10.0,9.0,9.0,19,1.64 +11974,85.0,9.0,9.0,10.0,10.0,9.0,9.0,22,2.08 +23908,77.0,8.0,8.0,10.0,10.0,10.0,9.0,13,1.11 +42797,90.0,9.0,6.0,10.0,10.0,10.0,10.0,2,0.17 +65937,90.0,9.0,10.0,9.0,9.0,9.0,9.0,14,1.37 +64458,80.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.17 +41953,96.0,10.0,9.0,10.0,10.0,10.0,9.0,9,0.79 +15676,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.26 +7671,,,,,,,,0, +4404,,,,,,,,0, +40611,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.88 +57991,,,,,,,,0, +65622,,,,,,,,0, +15280,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.15 +53896,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.1 +53684,80.0,8.0,9.0,9.0,9.0,8.0,8.0,7,0.61 +53865,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.47 +72327,98.0,9.0,10.0,10.0,10.0,10.0,10.0,8,0.7 +17703,,,,,,,,0, +28554,94.0,9.0,10.0,10.0,10.0,10.0,10.0,7,0.62 +36454,,,,,,,,0, +63294,88.0,9.0,9.0,9.0,10.0,9.0,9.0,16,1.37 +50205,71.0,7.0,9.0,8.0,7.0,9.0,8.0,10,0.96 +68936,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.23 +50470,,,,,,,,0, +64748,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.83 +20900,76.0,8.0,8.0,8.0,9.0,10.0,8.0,5,0.44 +58891,91.0,9.0,8.0,9.0,9.0,10.0,9.0,9,0.82 +73247,20.0,,,,,,,1,0.09 +5042,73.0,10.0,9.0,9.0,9.0,10.0,7.0,6,0.6 +59630,60.0,8.0,8.0,8.0,8.0,8.0,6.0,1,0.09 +68999,,,,,,,,0, +69331,,,,,,,,0, +75924,93.0,9.0,9.0,9.0,9.0,9.0,9.0,4,0.37 +62129,,,,,,,,0, +40479,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +10986,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.82 +442,93.0,10.0,9.0,10.0,10.0,10.0,10.0,51,4.9 +38274,80.0,9.0,8.0,10.0,9.0,10.0,8.0,8,0.7 +71804,96.0,10.0,9.0,9.0,10.0,10.0,10.0,15,1.32 +57530,95.0,10.0,10.0,10.0,10.0,9.0,10.0,45,3.99 +5870,93.0,10.0,10.0,10.0,10.0,9.0,10.0,53,4.85 +58140,,,,,,,,0, +6896,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.97 +36808,,,,,,,,0, +30516,60.0,6.0,6.0,8.0,6.0,8.0,6.0,3,0.26 +55869,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.81 +69286,98.0,10.0,10.0,10.0,10.0,9.0,10.0,11,0.97 +4618,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.35 +32141,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,2.08 +67462,90.0,9.0,9.0,10.0,10.0,10.0,9.0,66,5.74 +12348,87.0,9.0,9.0,9.0,9.0,10.0,9.0,62,5.45 +14729,90.0,10.0,8.0,10.0,10.0,10.0,10.0,6,0.75 +3603,90.0,9.0,8.0,10.0,10.0,9.0,10.0,2,0.18 +61904,98.0,9.0,9.0,10.0,10.0,9.0,10.0,13,1.7 +22539,99.0,10.0,10.0,10.0,10.0,10.0,10.0,37,3.54 +8258,78.0,8.0,7.0,9.0,9.0,9.0,8.0,11,0.98 +33935,,,,,,,,0, +9046,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +64570,100.0,10.0,10.0,10.0,10.0,9.0,10.0,14,1.24 +37380,,,,,,,,0, +44593,86.0,9.0,7.0,10.0,10.0,10.0,9.0,7,0.62 +63287,,,,,,,,0, +3510,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +68468,84.0,9.0,9.0,10.0,10.0,10.0,9.0,56,4.94 +36464,86.0,9.0,8.0,9.0,9.0,9.0,9.0,41,3.77 +3192,94.0,9.0,9.0,10.0,10.0,10.0,9.0,7,0.63 +53364,80.0,9.0,7.0,10.0,10.0,8.0,7.0,3,1.34 +50813,100.0,9.0,9.0,10.0,10.0,9.0,9.0,4,0.46 +34555,85.0,9.0,8.0,9.0,9.0,9.0,8.0,44,3.94 +66547,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.32 +69984,,,,,,,,0, +33564,,,,,,,,0, +45632,83.0,9.0,8.0,9.0,9.0,9.0,9.0,56,5.25 +38230,,,,,,,,0, +68629,100.0,10.0,10.0,10.0,10.0,9.0,10.0,12,1.31 +56743,91.0,10.0,9.0,9.0,10.0,9.0,9.0,12,1.06 +15640,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.22 +23857,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +7110,89.0,9.0,9.0,9.0,10.0,10.0,9.0,70,6.19 +18276,96.0,10.0,10.0,10.0,10.0,9.0,10.0,36,3.22 +27637,,,,,,,,0, +9911,94.0,9.0,9.0,9.0,9.0,9.0,9.0,7,0.67 +74163,93.0,9.0,10.0,10.0,10.0,9.0,9.0,29,2.54 +11118,,,,,,,,0, +41308,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.83 +11593,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.6 +12201,99.0,10.0,10.0,10.0,10.0,10.0,10.0,25,2.29 +58981,100.0,10.0,10.0,10.0,10.0,9.0,9.0,6,0.79 +62150,,,,,,,,0, +18772,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +27599,,,,,,,,0, +38540,80.0,10.0,10.0,9.0,8.0,8.0,9.0,4,0.45 +39733,,,,,,,,0, +54515,,,,,,,,1,0.12 +11338,80.0,7.0,10.0,7.0,8.0,10.0,8.0,3,0.3 +75009,98.0,9.0,10.0,10.0,9.0,9.0,10.0,10,0.91 +15358,,,,,,,,0, +40899,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.18 +54828,,,,,,,,1,0.09 +2751,96.0,10.0,8.0,10.0,10.0,10.0,10.0,5,0.46 +74203,99.0,10.0,10.0,10.0,10.0,9.0,10.0,15,1.47 +24469,,,,,,,,1,0.09 +69685,80.0,8.0,9.0,10.0,10.0,9.0,9.0,7,1.14 +48437,99.0,10.0,10.0,10.0,10.0,10.0,10.0,41,4.01 +66428,,,,,,,,0, +17278,,,,,,,,0, +1268,,,,,,,,0, +63599,77.0,9.0,8.0,9.0,9.0,9.0,8.0,41,3.82 +59645,93.0,10.0,9.0,10.0,10.0,10.0,9.0,18,1.68 +29719,70.0,8.0,7.0,9.0,10.0,9.0,8.0,3,0.28 +5065,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,1.7 +7104,93.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.72 +17837,89.0,10.0,8.0,10.0,10.0,10.0,9.0,9,0.86 +41473,99.0,10.0,10.0,10.0,10.0,10.0,10.0,39,4.08 +29201,,,,,,,,0, +71897,99.0,10.0,10.0,10.0,10.0,9.0,10.0,36,3.33 +5229,93.0,10.0,10.0,9.0,10.0,10.0,9.0,27,2.6 +39499,93.0,10.0,10.0,10.0,10.0,9.0,9.0,24,2.21 +69988,93.0,10.0,10.0,8.0,10.0,10.0,9.0,16,1.9 +15096,93.0,10.0,10.0,9.0,10.0,10.0,9.0,12,1.54 +3171,90.0,10.0,9.0,10.0,10.0,9.0,9.0,9,0.88 +39470,94.0,10.0,9.0,10.0,10.0,10.0,10.0,14,1.3 +23630,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.79 +34912,93.0,9.0,9.0,10.0,10.0,9.0,9.0,8,0.76 +27045,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +14017,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,1.99 +64065,100.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.23 +28248,60.0,6.0,6.0,6.0,6.0,5.0,5.0,2,0.18 +33665,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.46 +12242,93.0,9.0,10.0,10.0,10.0,9.0,9.0,16,1.54 +10197,85.0,9.0,9.0,10.0,9.0,9.0,8.0,26,3.7 +3600,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.49 +58676,97.0,10.0,9.0,10.0,10.0,9.0,10.0,83,7.66 +49645,97.0,10.0,10.0,10.0,10.0,10.0,10.0,58,5.42 +24539,97.0,10.0,10.0,10.0,10.0,10.0,10.0,51,4.69 +60551,92.0,9.0,9.0,10.0,10.0,10.0,9.0,83,7.76 +41621,86.0,9.0,9.0,9.0,9.0,10.0,9.0,32,2.92 +61247,,,,,,,,0, +5080,,,,,,,,0, +54085,,,,,,,,0, +28626,92.0,10.0,9.0,9.0,9.0,10.0,10.0,5,0.6 +41094,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.4 +4163,100.0,10.0,10.0,10.0,10.0,10.0,10.0,24,2.28 +73492,,,,,,,,0, +42264,,,,,,,,0, +76439,80.0,8.0,8.0,8.0,8.0,8.0,10.0,2,0.21 +27583,98.0,10.0,10.0,10.0,10.0,9.0,10.0,9,0.92 +36219,96.0,10.0,10.0,10.0,10.0,10.0,9.0,61,6.0 +65477,,,,,,,,1,0.09 +5813,96.0,10.0,9.0,9.0,10.0,10.0,10.0,5,0.48 +63960,70.0,9.0,6.0,6.0,10.0,9.0,6.0,2,0.19 +73594,,,,,,,,0, +63535,93.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.32 +33578,98.0,10.0,10.0,10.0,10.0,9.0,10.0,11,1.08 +16591,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.09 +73508,,,,,,,,0, +17995,60.0,6.0,8.0,10.0,10.0,6.0,6.0,1,0.1 +52098,99.0,10.0,10.0,10.0,10.0,10.0,10.0,37,3.7 +48134,98.0,10.0,10.0,10.0,10.0,10.0,10.0,25,2.55 +55714,96.0,10.0,10.0,10.0,10.0,9.0,9.0,6,0.58 +69393,100.0,8.0,8.0,10.0,10.0,10.0,8.0,1,0.11 +4106,77.0,8.0,7.0,8.0,8.0,9.0,8.0,27,2.48 +21660,81.0,8.0,8.0,8.0,8.0,9.0,8.0,14,1.43 +25642,98.0,10.0,10.0,10.0,10.0,10.0,9.0,12,1.14 +7940,100.0,10.0,10.0,10.0,10.0,10.0,9.0,18,1.76 +49850,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,1.05 +46970,,,,,,,,1,0.09 +4155,93.0,10.0,9.0,10.0,10.0,8.0,10.0,6,0.59 +8587,80.0,10.0,6.0,10.0,10.0,10.0,6.0,9,0.82 +28667,,,,,,,,0, +69324,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.32 +27798,98.0,10.0,10.0,10.0,9.0,10.0,9.0,8,1.92 +64396,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.1 +20207,73.0,7.0,5.0,9.0,9.0,8.0,7.0,6,0.58 +59456,,,,,,,,0, +38207,96.0,10.0,10.0,9.0,9.0,10.0,9.0,18,1.84 +53282,96.0,10.0,10.0,9.0,9.0,10.0,10.0,9,1.13 +69907,92.0,9.0,9.0,10.0,10.0,10.0,10.0,10,0.91 +6154,,,,,,,,0, +397,88.0,9.0,9.0,9.0,9.0,10.0,9.0,48,4.4 +56789,84.0,9.0,8.0,9.0,9.0,10.0,9.0,30,3.8 +14387,71.0,8.0,7.0,10.0,9.0,8.0,8.0,16,1.49 +26078,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,2.53 +57051,,,,,,,,0, +41995,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +55215,98.0,10.0,10.0,10.0,10.0,9.0,10.0,25,2.85 +33179,,,,,,,,0, +17160,91.0,9.0,10.0,10.0,9.0,10.0,9.0,28,2.64 +18200,100.0,9.0,9.0,10.0,9.0,10.0,10.0,3,0.35 +68688,91.0,10.0,9.0,10.0,10.0,9.0,10.0,10,0.96 +39847,70.0,8.0,6.0,10.0,10.0,10.0,7.0,2,1.58 +31889,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.48 +2303,40.0,3.0,2.0,4.0,5.0,7.0,3.0,2,0.8 +3283,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.81 +10637,100.0,10.0,10.0,10.0,10.0,9.0,9.0,7,0.68 +5444,,,,,,,,0, +53827,92.0,9.0,9.0,9.0,9.0,10.0,9.0,5,0.8 +17758,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.46 +57170,97.0,10.0,9.0,10.0,10.0,10.0,10.0,22,2.09 +74176,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +52893,98.0,10.0,10.0,10.0,10.0,10.0,10.0,30,2.98 +32997,93.0,9.0,9.0,10.0,10.0,10.0,9.0,12,1.43 +51970,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.03 +43802,80.0,9.0,9.0,9.0,10.0,9.0,9.0,4,0.4 +43168,,,,,,,,0, +76833,93.0,10.0,9.0,10.0,10.0,10.0,9.0,93,8.69 +40268,60.0,9.0,6.0,7.0,10.0,7.0,7.0,3,0.33 +15851,,,,,,,,0, +39657,,,,,,,,0, +64072,98.0,10.0,10.0,10.0,10.0,9.0,10.0,24,2.47 +63900,95.0,10.0,7.0,10.0,10.0,9.0,9.0,4,0.49 +8597,96.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.61 +26720,,,,,,,,1,0.13 +26357,91.0,10.0,10.0,10.0,10.0,10.0,9.0,7,0.71 +41517,93.0,9.0,9.0,10.0,10.0,10.0,9.0,7,0.68 +19891,87.0,9.0,9.0,9.0,9.0,10.0,8.0,12,1.24 +37859,73.0,9.0,8.0,10.0,10.0,10.0,10.0,4,0.41 +72241,91.0,9.0,9.0,10.0,10.0,9.0,10.0,7,0.67 +38043,40.0,4.0,5.0,5.0,6.0,7.0,6.0,2,0.19 +15390,100.0,10.0,10.0,10.0,10.0,10.0,9.0,13,1.36 +41747,,,,,,,,0, +5553,84.0,8.0,8.0,10.0,10.0,10.0,9.0,5,0.52 +22560,,,,,,,,0, +67891,100.0,10.0,9.0,10.0,10.0,9.0,10.0,8,0.82 +610,93.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.87 +38765,93.0,9.0,10.0,10.0,10.0,10.0,9.0,3,0.35 +2526,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.13 +15771,96.0,10.0,10.0,10.0,10.0,10.0,10.0,20,2.73 +47997,90.0,9.0,8.0,10.0,10.0,9.0,9.0,14,1.36 +1717,,,,,,,,0, +69137,,,,,,,,0, +20073,,,,,,,,0, +49681,,,,,,,,0, +58629,,,,,,,,0, +72485,,,,,,,,0, +75477,,,,,,,,0, +58980,,,,,,,,0, +18757,100.0,10.0,10.0,8.0,10.0,8.0,8.0,1,0.65 +53661,,,,,,,,0, +64609,,,,,,,,0, +52148,,,,,,,,0, +24728,,,,,,,,0, +7210,,,,,,,,0, +13564,,,,,,,,0, +7717,,,,,,,,0, +73650,,,,,,,,0, +64836,,,,,,,,0, +16878,,,,,,,,0, +5098,,,,,,,,0, +7407,,,,,,,,0, +9756,,,,,,,,0, +48674,,,,,,,,0, +50130,,,,,,,,0, +30423,,,,,,,,0, +66096,,,,,,,,0, +45970,,,,,,,,0, +34286,,,,,,,,0, +67603,,,,,,,,0, +34183,,,,,,,,0, +44881,,,,,,,,0, +27259,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.38 +21590,,,,,,,,0, +63730,,,,,,,,0, +58504,,,,,,,,0, +62243,,,,,,,,0, +61855,,,,,,,,0, +16110,,,,,,,,0, +14137,87.0,10.0,8.0,10.0,10.0,10.0,8.0,6,1.07 +53687,80.0,9.0,10.0,8.0,8.0,10.0,8.0,2,0.37 +53105,,,,,,,,0, +59380,,,,,,,,0, +50076,,,,,,,,0, +38227,,,,,,,,0, +52287,,,,,,,,0, +36251,,,,,,,,0, +57230,,,,,,,,0, +25706,,,,,,,,0, +53378,,,,,,,,0, +4339,,,,,,,,0, +15861,,,,,,,,0, +22310,,,,,,,,0, +72812,,,,,,,,0, +59636,93.0,10.0,9.0,10.0,10.0,10.0,9.0,85,7.97 +47505,,,,,,,,0, +41379,,,,,,,,0, +65860,,,,,,,,0, +2825,,,,,,,,0, +74070,,,,,,,,0, +8525,,,,,,,,0, +18714,,,,,,,,0, +49040,,,,,,,,0, +18523,,,,,,,,0, +67167,,,,,,,,0, +23531,86.0,9.0,8.0,10.0,10.0,9.0,9.0,21,2.46 +29311,,,,,,,,0, +17279,,,,,,,,0, +15890,,,,,,,,0, +43956,,,,,,,,0, +27696,94.0,9.0,9.0,10.0,10.0,10.0,10.0,70,6.54 +8992,,,,,,,,0, +27422,,,,,,,,0, +2521,,,,,,,,0, +39322,,,,,,,,0, +35881,,,,,,,,0, +62621,,,,,,,,0, +28835,,,,,,,,0, +12690,,,,,,,,0, +25002,,,,,,,,0, +58922,,,,,,,,0, +55284,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.19 +66826,,,,,,,,0, +13907,,,,,,,,0, +65681,,,,,,,,0, +75897,,,,,,,,0, +32345,,,,,,,,0, +50509,,,,,,,,0, +64693,,,,,,,,0, +54927,,,,,,,,0, +58628,80.0,9.0,9.0,10.0,9.0,10.0,9.0,3,0.55 +32079,,,,,,,,0, +44811,,,,,,,,0, +23254,,,,,,,,0, +4777,,,,,,,,0, +64181,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.2 +44548,,,,,,,,0, +19263,94.0,10.0,10.0,10.0,10.0,10.0,9.0,79,7.38 +17109,,,,,,,,0, +73740,,,,,,,,0, +69073,,,,,,,,0, +10420,90.0,10.0,7.0,10.0,10.0,10.0,9.0,6,0.63 +41884,93.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.46 +675,100.0,10.0,10.0,8.0,10.0,9.0,10.0,2,0.3 +58560,,,,,,,,0, +1845,90.0,10.0,9.0,10.0,10.0,9.0,9.0,41,5.17 +47626,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.2 +45530,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,1.84 +50462,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.23 +33717,60.0,6.0,6.0,8.0,6.0,4.0,4.0,1,0.13 +4544,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.14 +32631,93.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.61 +60560,,,,,,,,0, +63492,,,,,,,,0, +52175,76.0,8.0,7.0,9.0,9.0,9.0,8.0,18,1.73 +19322,80.0,9.0,8.0,10.0,9.0,9.0,8.0,7,0.73 +22575,60.0,5.0,7.0,8.0,5.0,8.0,6.0,3,0.31 +2350,,,,,,,,0, +40869,96.0,10.0,10.0,10.0,10.0,9.0,9.0,10,1.02 +17061,80.0,8.0,10.0,6.0,6.0,10.0,8.0,1,0.15 +60942,,,,,,,,0, +27941,95.0,10.0,10.0,10.0,10.0,8.0,10.0,8,0.78 +25833,,,,,,,,0, +41737,94.0,10.0,9.0,10.0,10.0,10.0,10.0,71,6.68 +26893,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +21205,92.0,10.0,9.0,10.0,9.0,10.0,9.0,62,5.92 +65482,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +60100,85.0,9.0,9.0,9.0,9.0,9.0,8.0,4,0.62 +74485,71.0,8.0,8.0,7.0,7.0,8.0,7.0,7,0.84 +6743,87.0,10.0,9.0,10.0,10.0,10.0,9.0,12,1.16 +26943,76.0,8.0,6.0,9.0,9.0,9.0,8.0,38,3.8 +40399,,,,,,,,0, +73453,60.0,7.0,7.0,7.0,6.0,9.0,5.0,3,0.3 +13258,,,,,,,,0, +62469,,,,,,,,1,0.25 +58323,95.0,10.0,10.0,9.0,10.0,10.0,9.0,17,1.83 +46314,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.2 +65720,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.23 +28257,76.0,8.0,8.0,8.0,8.0,8.0,8.0,5,0.56 +61605,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.74 +61707,84.0,9.0,8.0,10.0,10.0,9.0,8.0,10,1.13 +24027,80.0,10.0,7.0,8.0,9.0,10.0,8.0,5,0.5 +66114,,,,,,,,0, +72347,94.0,10.0,9.0,10.0,10.0,10.0,9.0,70,6.77 +73681,94.0,10.0,10.0,10.0,10.0,10.0,9.0,70,6.65 +67357,89.0,9.0,10.0,10.0,10.0,9.0,9.0,14,1.45 +13479,,,,,,,,0, +73040,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.45 +50231,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.72 +49754,93.0,10.0,9.0,10.0,9.0,9.0,10.0,3,0.33 +21887,98.0,10.0,9.0,10.0,10.0,10.0,9.0,10,1.13 +27439,,,,,,,,1,0.09 +22276,96.0,10.0,10.0,10.0,10.0,9.0,10.0,18,1.79 +15646,60.0,9.0,9.0,7.0,7.0,10.0,8.0,3,0.59 +7972,89.0,9.0,9.0,10.0,10.0,9.0,9.0,23,2.19 +71478,,,,,,,,0, +67176,,,,,,,,0, +47046,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.28 +73285,,,,,,,,0, +4981,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.1 +66314,,,,,,,,0, +45774,97.0,10.0,10.0,10.0,10.0,10.0,10.0,23,3.83 +76322,,,,,,,,0, +23068,100.0,10.0,9.0,10.0,10.0,9.0,10.0,7,0.69 +51894,93.0,10.0,9.0,10.0,10.0,10.0,10.0,6,2.61 +8225,90.0,10.0,9.0,9.0,9.0,10.0,9.0,2,0.2 +17392,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.1 +74204,97.0,10.0,9.0,10.0,10.0,9.0,9.0,21,2.05 +10051,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,0.44 +52138,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.01 +34756,88.0,9.0,9.0,10.0,10.0,10.0,9.0,10,1.55 +15988,93.0,10.0,9.0,10.0,10.0,9.0,9.0,6,0.68 +66070,92.0,9.0,10.0,10.0,9.0,10.0,9.0,41,4.01 +68277,95.0,10.0,9.0,10.0,10.0,9.0,10.0,38,3.61 +76484,96.0,10.0,10.0,10.0,10.0,9.0,10.0,11,1.08 +72338,,,,,,,,0, +54768,98.0,10.0,9.0,10.0,10.0,10.0,10.0,8,0.82 +49970,99.0,10.0,10.0,10.0,10.0,10.0,10.0,15,2.51 +63812,93.0,10.0,9.0,10.0,10.0,10.0,9.0,24,2.5 +54502,,,,,,,,0, +12920,94.0,9.0,9.0,10.0,10.0,9.0,9.0,20,2.01 +22814,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,1.2 +44836,98.0,10.0,10.0,10.0,10.0,10.0,10.0,19,1.87 +54295,,,,,,,,0, +3751,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.83 +33396,90.0,10.0,8.0,9.0,9.0,9.0,9.0,7,1.12 +36769,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +902,,,,,,,,0, +27531,70.0,7.0,7.0,10.0,8.0,9.0,7.0,2,0.23 +49610,,,,,,,,0, +68612,67.0,8.0,6.0,8.0,8.0,8.0,7.0,20,1.97 +49585,,,,,,,,0, +14427,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.07 +41330,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.27 +24126,87.0,8.0,10.0,10.0,8.0,10.0,7.0,3,0.41 +23334,98.0,10.0,10.0,10.0,10.0,9.0,10.0,11,1.31 +52929,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.24 +50309,,,,,,,,1, +43116,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,1.84 +68111,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +31422,,,,,,,,0, +48326,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +33318,,,,,,,,0, +16216,98.0,10.0,10.0,10.0,10.0,10.0,10.0,20,2.05 +13608,,,,,,,,1,0.11 +25857,,,,,,,,0, +48164,,,,,,,,0, +31081,,,,,,,,0, +16874,94.0,9.0,9.0,9.0,10.0,9.0,9.0,10,1.35 +62780,100.0,8.0,10.0,10.0,10.0,8.0,10.0,2,0.2 +65990,77.0,8.0,9.0,10.0,10.0,10.0,8.0,6,0.74 +4520,,,,,,,,0, +64271,,,,,,,,0, +36765,91.0,9.0,9.0,10.0,10.0,9.0,9.0,13,1.98 +53098,,,,,,,,0, +23094,,,,,,,,0, +21321,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,2.59 +2212,92.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.36 +28516,84.0,8.0,9.0,10.0,10.0,10.0,9.0,10,1.02 +42586,,,,,,,,0, +53514,,,,,,,,0, +37225,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.26 +34576,,,,,,,,0, +65819,85.0,8.0,8.0,9.0,9.0,10.0,8.0,22,2.23 +47262,80.0,8.0,10.0,10.0,10.0,8.0,8.0,2,0.2 +2768,,,,,,,,0, +9129,98.0,10.0,10.0,10.0,10.0,10.0,10.0,23,2.49 +3076,92.0,10.0,10.0,9.0,10.0,10.0,9.0,5,0.5 +42434,98.0,9.0,10.0,10.0,10.0,10.0,10.0,10,1.0 +74551,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.92 +51595,40.0,4.0,6.0,6.0,8.0,8.0,4.0,2,0.38 +39425,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.18 +60256,80.0,9.0,9.0,7.0,8.0,9.0,8.0,3,0.61 +8978,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.13 +67197,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,1.07 +5125,95.0,10.0,10.0,10.0,10.0,9.0,10.0,17,1.87 +47618,93.0,10.0,10.0,10.0,8.0,10.0,9.0,9,0.94 +68316,87.0,9.0,7.0,10.0,10.0,9.0,8.0,9,0.92 +46613,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.16 +84,83.0,9.0,10.0,8.0,7.0,10.0,8.0,6,0.75 +72989,96.0,10.0,10.0,10.0,10.0,9.0,9.0,18,2.21 +65840,100.0,10.0,10.0,10.0,10.0,10.0,10.0,19,2.23 +42531,,,,,,,,0, +11287,100.0,10.0,10.0,9.0,10.0,10.0,10.0,2,0.61 +59689,88.0,9.0,8.0,10.0,10.0,10.0,9.0,19,1.88 +63873,94.0,9.0,9.0,10.0,10.0,9.0,9.0,33,3.24 +47836,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.52 +37532,100.0,10.0,10.0,10.0,10.0,10.0,10.0,48,4.85 +36806,93.0,10.0,10.0,10.0,10.0,10.0,9.0,77,8.46 +21598,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,1.85 +13411,98.0,10.0,9.0,10.0,10.0,9.0,10.0,30,3.06 +68927,96.0,10.0,10.0,10.0,10.0,10.0,10.0,22,3.01 +45732,93.0,9.0,10.0,10.0,10.0,10.0,10.0,11,1.96 +55306,99.0,10.0,10.0,10.0,10.0,10.0,10.0,36,3.5 +33773,,,,,,,,0, +61973,,,,,,,,0, +28576,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.12 +20998,100.0,10.0,10.0,10.0,10.0,10.0,10.0,18,1.79 +48092,,,,,,,,0, +28980,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.45 +75441,,,,,,,,0, +76369,,,,,,,,0, +26203,99.0,10.0,10.0,10.0,10.0,10.0,9.0,15,1.61 +76744,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.42 +54084,,,,,,,,0, +42414,93.0,9.0,9.0,9.0,9.0,9.0,9.0,4,0.49 +71300,83.0,9.0,8.0,10.0,9.0,9.0,8.0,21,2.31 +12904,80.0,6.0,8.0,10.0,10.0,10.0,6.0,2,1.07 +76207,95.0,10.0,9.0,9.0,10.0,10.0,10.0,4,0.69 +15487,99.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.43 +66925,,,,,,,,0, +71713,,,,,,,,0, +57538,,,,,,,,0, +40826,,,,,,,,0, +11015,20.0,2.0,8.0,8.0,8.0,2.0,8.0,1,0.19 +63734,,,,,,,,0, +10815,89.0,9.0,9.0,8.0,9.0,9.0,9.0,9,0.92 +27849,94.0,10.0,9.0,10.0,10.0,10.0,9.0,28,3.15 +37286,98.0,10.0,10.0,10.0,10.0,9.0,10.0,25,2.47 +68730,70.0,10.0,5.0,10.0,10.0,9.0,9.0,2,0.22 +14921,,,,,,,,0, +68578,80.0,9.0,9.0,10.0,9.0,10.0,7.0,5,0.5 +7318,,,,,,,,0, +31274,92.0,10.0,10.0,10.0,9.0,8.0,9.0,10,1.53 +1155,,,,,,,,0, +2670,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.3 +26841,95.0,9.0,10.0,10.0,10.0,10.0,9.0,4,0.94 +67897,,,,,,,,0, +56696,,,,,,,,0, +22140,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.51 +4648,100.0,10.0,10.0,8.0,10.0,10.0,10.0,2,0.2 +68607,80.0,6.0,8.0,6.0,10.0,10.0,8.0,1,0.12 +23016,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.85 +24938,,,,,,,,1,0.1 +22497,,,,,,,,0, +40546,,,,,,,,0, +65261,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.43 +45966,95.0,10.0,10.0,10.0,10.0,10.0,10.0,9,0.91 +50780,,,,,,,,0, +18798,,,,,,,,0, +34645,,,,,,,,0, +9368,89.0,10.0,10.0,8.0,9.0,9.0,9.0,9,1.0 +73008,,,,,,,,0, +73414,,,,,,,,0, +61892,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.85 +13444,90.0,10.0,9.0,10.0,10.0,9.0,9.0,13,1.31 +12427,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.11 +70518,96.0,9.0,10.0,10.0,10.0,10.0,10.0,10,1.03 +33744,,,,,,,,0, +72764,100.0,10.0,10.0,10.0,10.0,10.0,10.0,23,2.32 +10181,91.0,10.0,9.0,10.0,10.0,9.0,9.0,11,1.21 +48229,,,,,,,,1,0.12 +17763,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.41 +39555,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.2 +2358,92.0,10.0,9.0,10.0,10.0,10.0,9.0,70,7.09 +32267,78.0,8.0,9.0,9.0,9.0,9.0,8.0,21,2.15 +160,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +19732,98.0,10.0,10.0,10.0,10.0,10.0,10.0,11,1.14 +73532,96.0,10.0,9.0,9.0,9.0,10.0,10.0,14,1.55 +26965,,,,,,,,0, +6230,93.0,10.0,9.0,10.0,10.0,8.0,9.0,18,2.25 +64555,,,,,,,,0, +69024,96.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.14 +24787,80.0,9.0,7.0,9.0,9.0,9.0,8.0,62,6.33 +12293,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.14 +34180,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.5 +69554,99.0,10.0,10.0,10.0,10.0,9.0,9.0,15,1.8 +63991,100.0,10.0,10.0,6.0,10.0,10.0,8.0,1,0.1 +53239,,,,,,,,0, +47852,,,,,,,,0, +12540,,,,,,,,0, +24239,80.0,8.0,8.0,10.0,10.0,10.0,8.0,4,0.51 +16291,88.0,9.0,9.0,9.0,9.0,10.0,8.0,13,1.55 +60912,93.0,10.0,9.0,10.0,10.0,9.0,9.0,6,0.6 +57974,88.0,9.0,10.0,10.0,10.0,10.0,9.0,21,2.15 +66703,87.0,9.0,9.0,10.0,10.0,9.0,9.0,3,0.53 +39142,80.0,7.0,7.0,10.0,9.0,10.0,8.0,2,0.38 +66415,70.0,8.0,9.0,10.0,7.0,10.0,7.0,6,0.63 +20485,100.0,8.0,10.0,10.0,10.0,10.0,10.0,2,0.28 +52484,,,,,,,,0, +18406,90.0,8.0,10.0,10.0,9.0,10.0,9.0,8,1.0 +27666,80.0,8.0,10.0,9.0,8.0,10.0,10.0,3,0.36 +13141,,,,,,,,0, +25731,86.0,9.0,8.0,9.0,9.0,9.0,9.0,13,1.39 +51486,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,1.44 +59217,,,,,,,,0, +70177,93.0,10.0,9.0,10.0,10.0,9.0,9.0,24,3.03 +73027,,,,,,,,0, +55644,,,,,,,,0, +18857,,,,,,,,0, +48976,,,,,,,,0, +30130,,,,,,,,0, +67343,,,,,,,,0, +11294,,,,,,,,0, +69707,,,,,,,,0, +48794,,,,,,,,0, +59441,100.0,10.0,10.0,9.0,10.0,10.0,10.0,11,1.29 +62665,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +247,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.2 +21547,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.38 +12155,94.0,10.0,9.0,10.0,10.0,9.0,9.0,10,1.15 +26723,100.0,10.0,10.0,9.0,10.0,10.0,10.0,4,0.4 +12231,,,,,,,,0, +37520,99.0,10.0,10.0,10.0,10.0,9.0,10.0,14,1.69 +20252,,,,,,,,0, +36088,,,,,,,,0, +6240,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.56 +31491,,,,,,,,0, +16871,,,,,,,,0, +35832,93.0,10.0,10.0,9.0,9.0,9.0,9.0,42,4.3 +15405,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.13 +10127,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.25 +18736,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.1 +28146,97.0,9.0,9.0,10.0,10.0,9.0,9.0,8,0.83 +32539,,,,,,,,0, +48854,80.0,10.0,8.0,10.0,6.0,8.0,10.0,1,0.11 +19312,94.0,10.0,9.0,10.0,10.0,9.0,9.0,7,0.71 +64025,100.0,10.0,9.0,10.0,10.0,9.0,9.0,2,0.21 +50438,89.0,9.0,10.0,9.0,9.0,9.0,9.0,40,4.2 +73920,85.0,9.0,6.0,8.0,9.0,10.0,8.0,5,0.89 +1400,100.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.37 +30623,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.94 +27675,,,,,,,,0, +38415,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.12 +68264,,,,,,,,0, +3211,20.0,2.0,6.0,2.0,2.0,6.0,6.0,1,0.1 +32068,,,,,,,,1,0.1 +40617,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.21 +29064,100.0,10.0,10.0,9.0,10.0,10.0,10.0,3,0.31 +21914,,,,,,,,0, +3553,95.0,9.0,9.0,10.0,9.0,10.0,10.0,17,1.74 +70713,,,,,,,,1,0.2 +38563,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,2.82 +32008,95.0,10.0,10.0,10.0,10.0,10.0,9.0,11,1.27 +57868,98.0,10.0,10.0,10.0,10.0,10.0,9.0,8,1.14 +44732,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.23 +6735,,,,,,,,0, +23552,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.12 +8836,,,,,,,,0, +14892,,,,,,,,0, +64412,,,,,,,,0, +58022,87.0,9.0,9.0,9.0,9.0,10.0,9.0,5,0.6 +53129,96.0,10.0,9.0,10.0,10.0,10.0,9.0,11,1.22 +50463,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,1.53 +299,89.0,9.0,9.0,10.0,10.0,10.0,9.0,13,1.72 +61936,96.0,10.0,10.0,10.0,10.0,10.0,9.0,11,1.34 +29593,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,2.06 +54987,100.0,10.0,10.0,8.0,8.0,8.0,10.0,1,0.27 +25258,,,,,,,,0, +17382,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +19862,,,,,,,,0, +67460,100.0,10.0,10.0,10.0,10.0,10.0,10.0,22,2.76 +9838,,,,,,,,0, +75031,,,,,,,,2, +71219,87.0,9.0,9.0,9.0,9.0,9.0,9.0,19,2.1 +17477,80.0,9.0,8.0,9.0,9.0,10.0,8.0,12,1.29 +72567,100.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.24 +21514,,,,,,,,0, +26085,,,,,,,,0, +26199,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +67946,93.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.41 +21176,,,,,,,,0, +26336,95.0,10.0,10.0,10.0,10.0,10.0,9.0,16,2.41 +62991,92.0,10.0,9.0,9.0,10.0,9.0,9.0,6,0.65 +48991,70.0,9.0,7.0,10.0,10.0,10.0,8.0,2,0.21 +53971,90.0,10.0,8.0,10.0,10.0,10.0,8.0,2,0.23 +36597,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.32 +65246,,,,,,,,2,0.21 +44851,100.0,10.0,9.0,9.0,10.0,10.0,10.0,3,0.39 +71688,90.0,9.0,9.0,9.0,9.0,10.0,9.0,16,1.77 +13136,95.0,10.0,10.0,10.0,10.0,10.0,9.0,24,2.79 +69857,96.0,10.0,10.0,10.0,10.0,10.0,10.0,42,4.32 +76244,,,,,,,,0, +4446,,,,,,,,1,0.11 +31082,88.0,9.0,9.0,9.0,9.0,9.0,9.0,13,1.55 +11586,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.51 +59736,,,,,,,,0, +17966,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.51 +7607,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.12 +19568,,,,,,,,0, +72398,,,,,,,,0, +23583,93.0,9.0,9.0,9.0,9.0,10.0,10.0,22,2.35 +59881,,,,,,,,0, +73929,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.35 +59771,,,,,,,,0, +22324,,,,,,,,0, +6714,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.7 +39396,89.0,10.0,10.0,10.0,9.0,10.0,8.0,16,1.68 +21624,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +23092,87.0,10.0,10.0,7.0,8.0,10.0,9.0,3,0.31 +29848,93.0,9.0,10.0,9.0,10.0,9.0,9.0,19,2.15 +41182,,,,,,,,0, +55651,97.0,10.0,10.0,10.0,10.0,10.0,10.0,29,3.12 +44400,97.0,10.0,9.0,10.0,10.0,9.0,10.0,14,1.47 +12956,97.0,10.0,10.0,10.0,10.0,10.0,9.0,13,2.24 +7473,98.0,10.0,10.0,10.0,10.0,10.0,9.0,10,1.22 +62575,93.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.34 +36057,91.0,10.0,10.0,10.0,10.0,9.0,9.0,17,2.22 +39688,97.0,10.0,10.0,10.0,10.0,9.0,10.0,37,3.98 +67729,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.61 +21403,40.0,6.0,5.0,10.0,6.0,6.0,5.0,2,0.21 +55167,,,,,,,,0, +72512,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.29 +46022,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.53 +8645,,,,,,,,0, +67513,93.0,10.0,10.0,10.0,9.0,10.0,9.0,3,0.34 +29504,94.0,9.0,9.0,10.0,9.0,9.0,9.0,29,3.23 +52079,,,,,,,,0, +10282,94.0,10.0,10.0,10.0,9.0,9.0,10.0,10,1.08 +31676,85.0,10.0,10.0,9.0,8.0,10.0,9.0,4,0.43 +6824,100.0,9.0,10.0,10.0,10.0,9.0,10.0,2,0.22 +34793,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.31 +71061,,,,,,,,0, +27936,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.43 +37907,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.5 +74834,85.0,8.0,10.0,10.0,9.0,10.0,9.0,6,0.62 +53158,,,,,,,,0, +42273,,,,,,,,0, +72190,80.0,6.0,8.0,8.0,6.0,6.0,6.0,1,0.14 +4392,90.0,9.0,10.0,10.0,10.0,10.0,10.0,2,0.48 +72699,97.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.68 +55082,90.0,10.0,10.0,9.0,10.0,10.0,10.0,2,0.24 +59525,98.0,10.0,10.0,10.0,10.0,10.0,10.0,26,2.73 +38060,87.0,9.0,8.0,10.0,10.0,9.0,9.0,10,1.15 +30772,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +66864,95.0,10.0,9.0,10.0,10.0,9.0,9.0,52,5.59 +20576,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.27 +23265,,,,,,,,0, +47248,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.17 +9855,85.0,9.0,8.0,9.0,9.0,8.0,9.0,22,2.34 +47370,85.0,9.0,9.0,10.0,9.0,8.0,9.0,21,2.19 +37967,93.0,10.0,9.0,10.0,10.0,9.0,9.0,23,2.39 +37236,82.0,9.0,9.0,9.0,9.0,8.0,8.0,17,1.79 +34547,89.0,9.0,8.0,9.0,10.0,8.0,9.0,19,1.99 +68278,80.0,10.0,9.0,10.0,10.0,8.0,9.0,2,0.98 +47069,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,1.51 +8842,97.0,10.0,10.0,10.0,10.0,9.0,10.0,12,2.45 +36443,,,,,,,,0, +76088,,,,,,,,2,0.3 +41129,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.4 +13700,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.57 +10674,98.0,10.0,10.0,10.0,10.0,10.0,10.0,59,6.44 +14483,,,,,,,,0, +30549,98.0,9.0,8.0,10.0,10.0,9.0,10.0,8,0.86 +68992,100.0,10.0,9.0,10.0,10.0,9.0,10.0,17,1.81 +75183,99.0,10.0,10.0,10.0,10.0,9.0,10.0,27,2.99 +25292,91.0,9.0,8.0,9.0,9.0,8.0,9.0,14,1.54 +13093,93.0,10.0,10.0,10.0,10.0,10.0,10.0,19,2.04 +18703,100.0,10.0,10.0,9.0,10.0,10.0,10.0,4,0.61 +70633,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.33 +1121,88.0,9.0,8.0,8.0,10.0,9.0,9.0,5,0.87 +15109,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +30027,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.34 +2675,85.0,10.0,7.0,9.0,9.0,9.0,8.0,19,2.82 +31964,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.68 +76885,93.0,9.0,9.0,10.0,10.0,10.0,10.0,3,0.5 +39720,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,2.23 +46889,90.0,10.0,10.0,10.0,10.0,10.0,9.0,12,1.25 +63116,,,,,,,,1,0.11 +66324,98.0,10.0,10.0,10.0,10.0,10.0,10.0,29,3.16 +36295,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.46 +17839,87.0,9.0,9.0,9.0,9.0,9.0,9.0,3,0.36 +76872,100.0,10.0,10.0,10.0,10.0,10.0,10.0,19,2.15 +69539,,,,,,,,0, +74970,87.0,9.0,9.0,7.0,9.0,10.0,9.0,19,2.31 +38244,100.0,8.0,8.0,8.0,10.0,10.0,10.0,1,0.11 +18280,,,,,,,,0, +46997,80.0,9.0,6.0,10.0,10.0,9.0,9.0,2,0.21 +17954,94.0,10.0,10.0,10.0,10.0,10.0,9.0,17,6.37 +55939,,,,,,,,0, +45226,,,,,,,,0, +2536,,,,,,,,0, +34032,,,,,,,,0, +69717,80.0,8.0,9.0,8.0,9.0,9.0,8.0,12,1.4 +74178,84.0,9.0,8.0,9.0,9.0,9.0,9.0,18,1.96 +28664,,,,,,,,0, +1647,100.0,10.0,10.0,9.0,10.0,10.0,10.0,12,1.43 +66843,97.0,9.0,10.0,10.0,9.0,9.0,10.0,7,0.86 +4134,93.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.83 +42959,,,,,,,,0, +5491,90.0,10.0,9.0,10.0,10.0,10.0,9.0,6,0.7 +1499,67.0,10.0,6.0,10.0,10.0,9.0,7.0,3,0.33 +5776,,,,,,,,0, +62330,,,,,,,,1,0.11 +75705,,,,,,,,0, +52272,94.0,9.0,10.0,9.0,9.0,9.0,9.0,21,2.7 +52145,100.0,10.0,10.0,10.0,9.0,10.0,9.0,6,0.84 +9307,,,,,,,,0, +10113,97.0,9.0,10.0,10.0,10.0,10.0,10.0,17,2.08 +24062,,,,,,,,0, +71849,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.13 +33884,,,,,,,,0, +70999,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,1.27 +9517,89.0,9.0,9.0,9.0,9.0,10.0,8.0,7,0.97 +2168,93.0,10.0,9.0,9.0,10.0,10.0,9.0,15,1.7 +42024,,,,,,,,0, +47949,,,,,,,,0, +76158,97.0,10.0,9.0,10.0,10.0,9.0,10.0,6,0.74 +43572,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.85 +17532,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.52 +58214,98.0,10.0,10.0,10.0,10.0,10.0,10.0,39,4.3 +76889,100.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.4 +5808,,,,,,,,0, +6478,90.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.28 +52877,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.22 +33375,90.0,8.0,9.0,9.0,10.0,9.0,9.0,7,1.06 +39344,,,,,,,,0, +31824,93.0,9.0,10.0,9.0,9.0,9.0,9.0,3,0.38 +43628,,,,,,,,0, +8183,,,,,,,,0, +3039,91.0,10.0,10.0,10.0,10.0,9.0,9.0,11,1.43 +47329,,,,,,,,0, +62143,,,,,,,,1,0.2 +59265,,,,,,,,0, +71336,,,,,,,,0, +66869,,,,,,,,0, +46935,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.41 +24096,84.0,8.0,8.0,8.0,8.0,10.0,8.0,6,0.69 +17205,84.0,9.0,8.0,10.0,10.0,9.0,9.0,10,1.17 +52772,87.0,7.0,7.0,9.0,7.0,9.0,10.0,4,0.44 +27746,,,,,,,,0, +51271,,,,,,,,0, +795,93.0,9.0,8.0,10.0,10.0,9.0,9.0,11,1.26 +20931,,,,,,,,0, +5225,100.0,10.0,9.0,10.0,10.0,9.0,9.0,2,0.24 +11835,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.44 +60057,67.0,7.0,6.0,7.0,7.0,8.0,7.0,3,0.37 +54024,,,,,,,,1,0.19 +33699,94.0,10.0,10.0,8.0,9.0,10.0,10.0,13,1.68 +68010,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.46 +43281,96.0,9.0,10.0,10.0,10.0,10.0,10.0,5,0.74 +64660,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.27 +31735,,,,,,,,0, +74892,80.0,9.0,10.0,9.0,7.0,9.0,8.0,2,0.48 +13825,,,,,,,,2,0.22 +71301,,,,,,,,0, +63596,,,,,,,,0, +2801,,,,,,,,0, +23139,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.23 +58565,80.0,7.0,7.0,10.0,10.0,9.0,7.0,3,0.43 +25248,,,,,,,,0, +50490,100.0,10.0,9.0,10.0,10.0,10.0,10.0,13,1.43 +29622,90.0,8.0,8.0,10.0,10.0,10.0,9.0,2,0.38 +23473,94.0,10.0,9.0,9.0,9.0,10.0,9.0,10,1.25 +60995,,,,,,,,0, +55790,97.0,10.0,10.0,10.0,10.0,10.0,10.0,15,1.96 +64882,20.0,4.0,2.0,4.0,6.0,6.0,2.0,1,0.11 +19066,84.0,10.0,9.0,10.0,10.0,10.0,9.0,5,0.77 +45734,93.0,10.0,8.0,10.0,10.0,10.0,10.0,47,6.47 +51780,93.0,10.0,10.0,10.0,10.0,8.0,9.0,11,1.34 +50530,,,,,,,,0, +2003,,,,,,,,1,0.14 +47136,89.0,9.0,8.0,10.0,10.0,9.0,9.0,39,4.4 +76270,99.0,10.0,10.0,10.0,10.0,10.0,10.0,20,2.27 +60049,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.17 +15811,90.0,9.0,9.0,9.0,9.0,10.0,9.0,21,2.31 +53784,90.0,10.0,9.0,10.0,10.0,9.0,9.0,14,1.58 +70586,96.0,10.0,10.0,9.0,10.0,10.0,9.0,19,2.28 +54609,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.55 +11580,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.6 +32826,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.56 +33579,,,,,,,,0, +18529,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.01 +65268,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.34 +63921,90.0,9.0,9.0,10.0,10.0,10.0,9.0,8,0.88 +66664,,,,,,,,1,0.11 +20675,96.0,10.0,8.0,9.0,10.0,10.0,10.0,5,0.62 +50276,,,,,,,,1,0.11 +28252,100.0,10.0,9.0,10.0,10.0,9.0,9.0,4,0.56 +21765,80.0,4.0,8.0,8.0,6.0,8.0,6.0,1,0.12 +68082,97.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.93 +27473,93.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.4 +1491,,,,,,,,0, +10668,,,,,,,,2,0.22 +53257,93.0,10.0,9.0,9.0,10.0,10.0,9.0,64,7.19 +38449,,,,,,,,1,0.11 +65851,96.0,10.0,9.0,10.0,10.0,10.0,9.0,10,1.1 +55360,,,,,,,,0, +48663,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.63 +53443,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.33 +70175,,,,,,,,1,0.18 +13391,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.13 +74846,97.0,10.0,10.0,10.0,10.0,10.0,10.0,20,2.33 +70557,90.0,9.0,10.0,10.0,10.0,10.0,9.0,12,1.35 +40702,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.12 +64022,,,,,,,,0, +50254,79.0,9.0,9.0,8.0,10.0,9.0,8.0,16,1.76 +63079,76.0,8.0,5.0,10.0,9.0,9.0,8.0,5,0.68 +55446,,,,,,,,0, +30355,93.0,10.0,9.0,10.0,10.0,10.0,10.0,9,1.34 +72504,85.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.44 +74250,96.0,10.0,9.0,10.0,10.0,9.0,10.0,19,2.1 +8482,,,,,,,,0, +44109,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.32 +16703,,,,,,,,1,0.11 +70820,89.0,9.0,8.0,9.0,9.0,9.0,9.0,23,2.91 +56977,,,,,,,,0, +23365,,,,,,,,0, +56094,100.0,10.0,10.0,10.0,10.0,10.0,9.0,11,1.27 +48162,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.67 +51841,76.0,9.0,9.0,9.0,9.0,10.0,8.0,11,1.28 +36781,96.0,10.0,9.0,10.0,10.0,10.0,10.0,17,2.04 +25878,87.0,9.0,9.0,8.0,9.0,9.0,8.0,10,1.2 +20858,92.0,10.0,9.0,9.0,10.0,10.0,9.0,13,1.88 +13621,87.0,9.0,8.0,8.0,9.0,10.0,8.0,6,0.78 +21934,87.0,10.0,10.0,10.0,10.0,10.0,9.0,9,1.02 +44879,88.0,9.0,10.0,10.0,10.0,10.0,9.0,5,0.6 +19079,81.0,9.0,7.0,9.0,9.0,9.0,8.0,15,1.74 +30026,97.0,10.0,10.0,10.0,10.0,10.0,10.0,29,3.78 +54543,85.0,10.0,9.0,10.0,10.0,9.0,9.0,5,0.57 +33945,,,,,,,,0, +22034,,,,,,,,0, +73192,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.15 +76455,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.24 +13251,98.0,10.0,10.0,10.0,10.0,10.0,9.0,13,1.42 +52862,96.0,10.0,10.0,9.0,10.0,10.0,10.0,11,1.2 +62727,90.0,9.0,9.0,8.0,9.0,8.0,8.0,3,0.33 +45491,,,,,,,,0, +35973,50.0,4.0,5.0,3.0,3.0,5.0,6.0,2,0.28 +988,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.15 +54824,,,,,,,,0, +52099,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.57 +12994,82.0,9.0,9.0,10.0,10.0,10.0,9.0,25,2.93 +30532,87.0,9.0,9.0,10.0,10.0,9.0,9.0,33,3.68 +22398,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.12 +17891,81.0,9.0,8.0,9.0,9.0,9.0,8.0,26,2.91 +18966,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,1.44 +76770,,,,,,,,0, +37352,87.0,10.0,10.0,9.0,10.0,8.0,9.0,12,1.99 +40816,77.0,9.0,9.0,8.0,9.0,9.0,7.0,14,1.68 +9567,80.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.13 +37148,100.0,10.0,10.0,10.0,10.0,10.0,7.0,3,0.43 +65954,,,,,,,,0, +68269,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.47 +25472,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.4 +62402,90.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.27 +53374,94.0,10.0,10.0,10.0,10.0,10.0,10.0,58,7.07 +71945,80.0,9.0,9.0,9.0,9.0,9.0,9.0,14,2.09 +42825,,,,,,,,0, +43247,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.57 +68281,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.88 +1182,91.0,9.0,9.0,9.0,9.0,10.0,9.0,7,0.77 +41021,92.0,10.0,10.0,10.0,9.0,10.0,10.0,12,1.48 +32331,,,,,,,,0, +60077,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,2.03 +4547,,,,,,,,0, +31636,,,,,,,,0, +8271,,,,,,,,0, +3121,89.0,9.0,9.0,10.0,9.0,9.0,9.0,17,2.02 +29037,87.0,9.0,8.0,9.0,9.0,9.0,9.0,22,2.66 +32879,74.0,8.0,7.0,9.0,9.0,8.0,8.0,14,6.89 +3300,88.0,9.0,8.0,10.0,10.0,9.0,9.0,19,2.3 +76197,97.0,10.0,9.0,9.0,9.0,10.0,10.0,9,1.11 +62043,,,,,,,,0, +69934,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.34 +694,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.91 +25074,95.0,10.0,10.0,10.0,9.0,10.0,10.0,4,0.47 +37054,,,,,,,,1,0.11 +63823,,,,,,,,0, +15082,,,,,,,,0, +29939,,,,,,,,0, +34013,,,,,,,,0, +10308,,,,,,,,0, +50518,89.0,9.0,9.0,10.0,10.0,9.0,9.0,11,1.96 +72319,91.0,10.0,10.0,9.0,10.0,10.0,9.0,17,4.77 +14449,,,,,,,,0, +33272,,,,,,,,0, +74790,,,,,,,,0, +39927,100.0,9.0,10.0,10.0,10.0,9.0,9.0,2,0.26 +72498,87.0,10.0,9.0,7.0,9.0,9.0,9.0,3,0.52 +75410,92.0,9.0,10.0,9.0,9.0,10.0,9.0,6,0.75 +29152,91.0,10.0,10.0,9.0,10.0,9.0,10.0,9,1.06 +11373,,,,,,,,0, +1609,98.0,10.0,10.0,10.0,10.0,10.0,10.0,24,2.94 +45100,,,,,,,,0, +24271,83.0,10.0,9.0,10.0,9.0,10.0,8.0,8,1.03 +39219,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.96 +28284,,,,,,,,0, +16382,86.0,10.0,10.0,10.0,9.0,10.0,9.0,17,1.99 +22518,91.0,10.0,9.0,9.0,9.0,8.0,9.0,11,1.3 +75583,,,,,,,,0, +55597,,,,,,,,0, +41336,90.0,8.0,9.0,10.0,10.0,9.0,9.0,3,0.38 +58727,83.0,9.0,8.0,9.0,9.0,8.0,8.0,23,2.58 +31256,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.51 +35225,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,1.48 +68627,,,,,,,,0, +6182,,,,,,,,0, +62033,,,,,,,,0, +22918,,,,,,,,0, +17933,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.34 +65871,,,,,,,,0, +24331,,,,,,,,0, +44190,,,,,,,,0, +45327,94.0,10.0,10.0,10.0,10.0,10.0,10.0,10,1.29 +26927,80.0,6.0,6.0,10.0,10.0,8.0,8.0,1,0.13 +25585,,,,,,,,0, +63212,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.33 +62077,99.0,10.0,10.0,10.0,10.0,9.0,10.0,27,3.13 +12428,98.0,10.0,10.0,10.0,10.0,10.0,10.0,17,1.98 +29646,100.0,10.0,10.0,9.0,10.0,10.0,10.0,2,0.36 +39084,88.0,9.0,9.0,9.0,9.0,9.0,9.0,22,2.58 +73620,,,,,,,,0, +46848,80.0,10.0,8.0,10.0,10.0,10.0,6.0,3,1.29 +265,93.0,9.0,10.0,10.0,10.0,9.0,8.0,3,0.46 +60452,92.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.61 +72444,97.0,10.0,10.0,10.0,10.0,10.0,10.0,20,2.4 +22200,90.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.29 +73628,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.23 +1872,93.0,9.0,9.0,10.0,10.0,10.0,9.0,3,0.46 +4642,,,,,,,,0, +41495,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.24 +11473,,,,,,,,0, +3749,83.0,9.0,9.0,10.0,9.0,9.0,9.0,6,0.9 +35886,,,,,,,,0, +74555,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.16 +75142,,,,,,,,0, +61554,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.34 +69968,73.0,7.0,5.0,8.0,10.0,10.0,9.0,3,0.36 +15497,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.55 +22032,93.0,10.0,10.0,10.0,10.0,9.0,9.0,20,2.74 +6223,91.0,10.0,9.0,9.0,10.0,9.0,9.0,9,1.08 +53456,94.0,9.0,10.0,9.0,9.0,10.0,9.0,7,0.98 +21633,77.0,10.0,7.0,9.0,9.0,9.0,7.0,6,0.85 +23359,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.47 +9070,95.0,10.0,9.0,10.0,10.0,9.0,10.0,55,6.55 +22753,97.0,10.0,10.0,10.0,10.0,10.0,10.0,27,3.09 +75796,93.0,10.0,10.0,10.0,9.0,8.0,10.0,3,0.37 +11469,96.0,10.0,10.0,10.0,9.0,10.0,10.0,29,3.28 +59407,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.11 +49076,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,0.42 +39533,78.0,9.0,8.0,10.0,9.0,9.0,8.0,9,1.18 +5034,,,,,,,,0, +25813,100.0,10.0,4.0,10.0,10.0,10.0,8.0,1,0.58 +61283,,,,,,,,0, +36218,80.0,10.0,10.0,7.0,10.0,9.0,8.0,2,0.29 +6299,94.0,10.0,10.0,10.0,10.0,10.0,9.0,13,3.07 +4830,96.0,10.0,10.0,9.0,9.0,10.0,10.0,9,1.22 +31058,92.0,10.0,9.0,10.0,10.0,9.0,8.0,13,1.6 +45351,,,,,,,,0, +70129,80.0,6.0,9.0,7.0,8.0,10.0,6.0,4,0.48 +28075,95.0,10.0,9.0,10.0,10.0,10.0,10.0,8,1.14 +2121,90.0,9.0,8.0,10.0,10.0,9.0,10.0,14,2.19 +57415,100.0,10.0,10.0,10.0,10.0,10.0,10.0,42,5.58 +32919,,,,,,,,0, +68265,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.24 +16839,,,,,,,,1,0.12 +55964,100.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.62 +49090,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.43 +59366,83.0,8.0,7.0,9.0,9.0,8.0,9.0,6,0.72 +59699,67.0,10.0,7.0,9.0,9.0,10.0,8.0,3,0.55 +75687,,,,,,,,1,0.11 +19433,96.0,10.0,10.0,8.0,9.0,10.0,9.0,15,1.91 +1868,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.85 +27864,97.0,10.0,9.0,10.0,10.0,10.0,10.0,7,0.81 +23040,100.0,10.0,9.0,10.0,10.0,9.0,10.0,2,0.25 +39312,70.0,9.0,8.0,8.0,10.0,10.0,7.0,10,1.48 +63985,100.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.48 +41264,84.0,9.0,9.0,9.0,9.0,10.0,9.0,11,1.48 +18243,,,,,,,,0, +72845,100.0,10.0,8.0,10.0,10.0,8.0,10.0,1,0.15 +48357,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.58 +59068,93.0,10.0,9.0,10.0,10.0,9.0,10.0,18,2.87 +10464,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,1.73 +10498,98.0,10.0,9.0,10.0,10.0,9.0,10.0,12,2.18 +22130,85.0,9.0,8.0,9.0,8.0,10.0,9.0,8,1.11 +54399,97.0,10.0,9.0,10.0,10.0,10.0,10.0,27,3.16 +67251,88.0,9.0,8.0,10.0,10.0,9.0,8.0,5,0.67 +54152,95.0,10.0,9.0,9.0,10.0,9.0,10.0,4,0.56 +60418,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.82 +5285,,,,,,,,0, +21437,,,,,,,,0, +4251,93.0,9.0,9.0,10.0,10.0,9.0,9.0,27,3.38 +54929,97.0,10.0,10.0,10.0,10.0,10.0,9.0,12,1.51 +2686,,,,,,,,0, +20317,90.0,10.0,10.0,9.0,9.0,9.0,9.0,16,1.93 +70079,,,,,,,,0, +39860,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +36907,,,,,,,,0, +21125,97.0,10.0,10.0,10.0,10.0,10.0,9.0,25,2.92 +36222,99.0,10.0,10.0,10.0,10.0,10.0,10.0,53,7.07 +49818,,,,,,,,1,0.15 +57735,76.0,8.0,7.0,8.0,8.0,8.0,8.0,5,0.72 +21561,99.0,10.0,10.0,10.0,10.0,9.0,10.0,23,3.0 +14167,94.0,10.0,10.0,10.0,10.0,10.0,10.0,11,1.35 +20238,90.0,9.0,9.0,10.0,9.0,9.0,9.0,2,0.25 +49587,80.0,7.0,7.0,10.0,10.0,10.0,8.0,3,0.35 +4834,100.0,10.0,8.0,10.0,10.0,6.0,8.0,1,0.15 +19944,80.0,8.0,9.0,9.0,9.0,9.0,9.0,2,0.34 +69524,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.75 +44402,90.0,10.0,10.0,9.0,9.0,9.0,9.0,25,3.36 +54956,,,,,,,,0, +42068,20.0,2.0,2.0,2.0,2.0,6.0,2.0,1,0.12 +49826,,,,,,,,0, +1834,90.0,10.0,8.0,10.0,10.0,10.0,10.0,2,2.0 +2376,88.0,10.0,10.0,10.0,10.0,10.0,9.0,8,1.11 +55664,93.0,10.0,10.0,10.0,9.0,9.0,9.0,18,2.2 +54095,100.0,10.0,10.0,9.0,10.0,10.0,10.0,4,0.53 +66312,90.0,10.0,9.0,9.0,9.0,10.0,9.0,46,5.5 +17696,93.0,9.0,10.0,10.0,10.0,9.0,9.0,9,1.32 +14920,80.0,9.0,8.0,10.0,10.0,10.0,9.0,3,0.36 +75847,84.0,9.0,8.0,9.0,9.0,10.0,9.0,9,1.42 +6531,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.39 +67919,,,,,,,,1,0.11 +73907,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.73 +16428,,,,,,,,0, +40601,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.53 +33609,,,,,,,,0, +31979,93.0,10.0,9.0,10.0,10.0,9.0,10.0,4,0.48 +73748,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.15 +26186,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.52 +25880,,,,,,,,0, +40092,,,,,,,,0, +21369,,,,,,,,0, +71766,,,,,,,,0, +51758,,,,,,,,0, +24990,,,,,,,,0, +40880,93.0,9.0,9.0,10.0,10.0,10.0,9.0,7,0.87 +51964,71.0,8.0,8.0,9.0,10.0,7.0,9.0,7,0.86 +50446,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.68 +39886,95.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.56 +41163,,,,,,,,0, +55886,92.0,9.0,10.0,10.0,10.0,10.0,9.0,5,0.67 +67241,96.0,10.0,10.0,10.0,10.0,9.0,10.0,28,3.4 +48935,100.0,10.0,10.0,10.0,10.0,10.0,10.0,19,2.6 +28807,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +572,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.26 +57383,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.7 +42551,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.67 +73322,88.0,9.0,10.0,9.0,9.0,10.0,9.0,12,1.68 +50844,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.42 +27767,96.0,10.0,10.0,10.0,10.0,9.0,9.0,18,2.51 +17266,89.0,10.0,9.0,9.0,10.0,10.0,9.0,15,1.86 +73147,100.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.36 +20572,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.14 +10093,86.0,9.0,9.0,9.0,9.0,8.0,9.0,7,1.35 +74205,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.39 +66848,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.38 +71307,100.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.52 +15718,100.0,10.0,10.0,10.0,8.0,8.0,10.0,1,0.48 +44675,80.0,8.0,7.0,9.0,8.0,10.0,8.0,4,1.08 +52486,,,,,,,,0, +69197,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.48 +2630,96.0,10.0,8.0,10.0,10.0,10.0,10.0,9,3.51 +6083,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.24 +42459,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.72 +54941,80.0,4.0,6.0,8.0,6.0,4.0,6.0,1,0.12 +30003,,,,,,,,0, +31225,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.14 +30703,,,,,,,,0, +13,,,,,,,,0, +30296,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.75 +54607,,,,,,,,0, +71565,,,,,,,,0, +27894,88.0,9.0,9.0,8.0,8.0,10.0,9.0,5,0.64 +48921,88.0,10.0,9.0,9.0,9.0,10.0,9.0,15,1.9 +62098,100.0,9.0,9.0,9.0,9.0,9.0,9.0,3,0.36 +45522,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.17 +7608,84.0,8.0,9.0,9.0,9.0,10.0,9.0,10,1.3 +58803,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.77 +10268,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.7 +5419,96.0,10.0,10.0,9.0,10.0,9.0,9.0,5,0.65 +3711,76.0,8.0,8.0,8.0,9.0,10.0,8.0,10,1.22 +70241,,,,,,,,0, +65287,83.0,9.0,8.0,10.0,10.0,9.0,9.0,9,1.29 +48839,100.0,10.0,10.0,10.0,9.0,8.0,10.0,2,0.25 +32009,91.0,9.0,9.0,10.0,10.0,10.0,9.0,17,2.35 +19452,93.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.64 +25447,90.0,10.0,10.0,9.0,9.0,10.0,9.0,2,2.0 +28963,90.0,10.0,10.0,10.0,8.0,9.0,8.0,2,0.52 +66491,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.3 +51029,98.0,10.0,10.0,10.0,10.0,10.0,9.0,8,1.18 +26788,88.0,9.0,10.0,10.0,10.0,8.0,8.0,5,0.63 +16733,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.28 +25565,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.71 +3,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.47 +31924,,,,,,,,0, +22680,,,,,,,,0, +31471,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.25 +46391,82.0,9.0,10.0,9.0,8.0,10.0,8.0,14,1.8 +11495,93.0,9.0,9.0,9.0,10.0,10.0,9.0,12,1.42 +76056,96.0,9.0,10.0,9.0,10.0,9.0,10.0,19,2.5 +55212,,,,,,,,0, +15760,100.0,10.0,9.0,10.0,10.0,10.0,9.0,7,0.89 +44677,75.0,9.0,7.0,10.0,10.0,8.0,8.0,5,0.6 +74868,,,,,,,,0, +54400,87.0,9.0,8.0,9.0,8.0,10.0,9.0,7,0.86 +66467,88.0,9.0,9.0,10.0,9.0,10.0,9.0,10,1.19 +65036,96.0,10.0,10.0,10.0,10.0,10.0,10.0,35,5.38 +56356,97.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.72 +5827,,,,,,,,0, +36871,93.0,10.0,10.0,10.0,10.0,9.0,10.0,14,1.81 +17451,95.0,10.0,10.0,10.0,9.0,9.0,10.0,4,0.57 +42292,90.0,9.0,10.0,10.0,9.0,9.0,8.0,6,0.78 +67277,80.0,9.0,9.0,9.0,9.0,8.0,9.0,51,6.43 +21052,,,,,,,,0, +27241,100.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.55 +48873,95.0,10.0,9.0,10.0,10.0,10.0,10.0,17,2.09 +33873,100.0,10.0,10.0,10.0,10.0,9.0,9.0,7,0.89 +29697,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.62 +4409,100.0,10.0,9.0,10.0,10.0,10.0,10.0,7,1.24 +18176,98.0,10.0,10.0,10.0,10.0,10.0,10.0,36,4.7 +70571,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.39 +15441,73.0,8.0,9.0,8.0,9.0,8.0,7.0,20,2.65 +26930,,,,,,,,0, +456,,,,,,,,0, +19601,94.0,10.0,9.0,10.0,10.0,10.0,9.0,13,1.78 +28050,97.0,10.0,9.0,9.0,9.0,10.0,9.0,7,0.9 +39428,96.0,10.0,10.0,10.0,10.0,9.0,9.0,12,1.6 +42085,100.0,10.0,10.0,10.0,10.0,10.0,9.0,11,2.13 +6218,20.0,2.0,2.0,10.0,2.0,2.0,2.0,1,0.79 +56412,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,0.98 +55732,,,,,,,,0, +22452,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.36 +1281,98.0,10.0,10.0,10.0,10.0,9.0,10.0,25,3.15 +29158,100.0,10.0,10.0,10.0,10.0,10.0,10.0,18,2.28 +17314,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,2.93 +37324,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.29 +4418,,,,,,,,0, +33065,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.14 +29839,100.0,9.0,9.0,10.0,10.0,10.0,10.0,7,1.03 +7628,,,,,,,,0, +32,98.0,10.0,10.0,10.0,10.0,10.0,10.0,19,2.49 +45140,78.0,9.0,9.0,9.0,10.0,9.0,8.0,12,1.65 +16023,88.0,9.0,9.0,9.0,9.0,9.0,9.0,10,1.35 +5201,,,,,,,,0, +39767,100.0,8.0,10.0,10.0,10.0,8.0,10.0,2,0.24 +12472,83.0,10.0,8.0,10.0,9.0,10.0,9.0,12,1.61 +74806,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.38 +20888,93.0,10.0,10.0,10.0,10.0,10.0,9.0,9,1.53 +49483,89.0,10.0,9.0,10.0,10.0,10.0,9.0,7,1.06 +23675,89.0,9.0,9.0,10.0,10.0,9.0,9.0,11,1.51 +45368,87.0,9.0,7.0,10.0,10.0,9.0,8.0,6,0.81 +62475,90.0,9.0,8.0,9.0,10.0,9.0,9.0,6,0.78 +8948,,,,,,,,0, +44243,97.0,10.0,10.0,10.0,10.0,9.0,9.0,13,1.7 +29919,95.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.57 +50511,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.5 +66556,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.36 +75521,92.0,9.0,10.0,10.0,10.0,9.0,9.0,17,2.52 +73318,95.0,9.0,9.0,10.0,10.0,9.0,10.0,12,1.73 +20585,,,,,,,,2,0.24 +64424,90.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.26 +8552,82.0,9.0,9.0,10.0,9.0,9.0,8.0,20,3.16 +7111,93.0,10.0,9.0,9.0,9.0,10.0,9.0,3,0.52 +53165,97.0,10.0,10.0,10.0,9.0,10.0,10.0,14,1.94 +75810,,,,,,,,1,0.13 +36363,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.83 +19209,99.0,10.0,10.0,10.0,10.0,10.0,9.0,33,4.18 +2555,88.0,9.0,10.0,9.0,10.0,9.0,9.0,5,0.65 +25026,,,,,,,,0, +57471,90.0,10.0,9.0,10.0,9.0,10.0,9.0,6,0.76 +41261,98.0,10.0,10.0,10.0,10.0,10.0,9.0,16,2.3 +21472,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.37 +49937,,,,,,,,0, +49519,,,,,,,,0, +9204,87.0,9.0,8.0,8.0,8.0,10.0,9.0,8,1.1 +49978,89.0,10.0,9.0,10.0,10.0,10.0,8.0,16,2.22 +13974,98.0,10.0,10.0,10.0,10.0,10.0,10.0,53,6.82 +38220,89.0,9.0,9.0,9.0,10.0,10.0,9.0,16,2.65 +19640,95.0,10.0,9.0,10.0,10.0,10.0,10.0,39,4.81 +40673,95.0,10.0,9.0,10.0,10.0,10.0,9.0,4,1.38 +70877,98.0,10.0,10.0,10.0,10.0,9.0,10.0,13,1.7 +23081,70.0,8.0,8.0,9.0,8.0,9.0,7.0,2,0.31 +64476,95.0,10.0,10.0,10.0,10.0,9.0,10.0,8,1.08 +7802,94.0,10.0,10.0,10.0,10.0,9.0,9.0,7,0.92 +25194,88.0,9.0,9.0,9.0,9.0,8.0,9.0,69,8.59 +41532,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,1.3 +1619,78.0,8.0,8.0,9.0,10.0,10.0,8.0,17,2.36 +75343,95.0,10.0,9.0,10.0,10.0,9.0,10.0,12,1.53 +45576,97.0,10.0,10.0,9.0,10.0,10.0,9.0,12,1.48 +13897,87.0,9.0,8.0,10.0,10.0,9.0,8.0,12,1.66 +18181,99.0,10.0,10.0,10.0,10.0,10.0,10.0,22,2.78 +40514,,,,,,,,0, +60395,94.0,10.0,9.0,10.0,10.0,10.0,10.0,48,5.83 +42245,88.0,9.0,10.0,9.0,10.0,8.0,9.0,6,0.79 +72724,89.0,9.0,9.0,9.0,9.0,9.0,9.0,16,2.03 +72018,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,1.9 +43829,,,,,,,,0, +19435,,,,,,,,0, +46796,88.0,9.0,9.0,10.0,9.0,9.0,9.0,16,1.96 +37913,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.45 +67615,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.53 +27337,,,,,,,,1,0.13 +44865,98.0,10.0,9.0,10.0,10.0,10.0,10.0,26,3.39 +57236,89.0,9.0,9.0,10.0,9.0,9.0,9.0,12,1.46 +57959,,,,,,,,0, +9460,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.52 +10064,93.0,10.0,10.0,10.0,9.0,9.0,9.0,9,1.33 +39253,86.0,9.0,9.0,9.0,10.0,9.0,9.0,52,6.34 +44348,89.0,9.0,9.0,10.0,9.0,10.0,9.0,44,5.39 +14461,100.0,10.0,10.0,10.0,10.0,9.0,10.0,40,5.02 +6283,90.0,9.0,10.0,10.0,10.0,9.0,9.0,8,1.15 +6381,94.0,10.0,9.0,9.0,10.0,10.0,9.0,10,1.41 +49135,80.0,10.0,10.0,8.0,6.0,10.0,10.0,1,0.16 +61878,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,1.67 +36545,75.0,8.0,7.0,8.0,8.0,10.0,8.0,4,0.89 +34769,,,,,,,,0, +17921,100.0,8.0,10.0,8.0,8.0,10.0,10.0,4,0.5 +29744,93.0,10.0,9.0,10.0,10.0,10.0,10.0,18,2.67 +52125,50.0,5.0,5.0,6.0,6.0,5.0,6.0,2,0.25 +33144,,,,,,,,0, +18921,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.39 +28676,,,,,,,,0, +51904,,,,,,,,0, +6278,,,,,,,,0, +46731,73.0,9.0,7.0,10.0,9.0,9.0,8.0,35,4.41 +2145,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.75 +64200,88.0,9.0,9.0,10.0,9.0,10.0,9.0,51,6.3 +13458,87.0,9.0,9.0,10.0,9.0,10.0,9.0,47,5.8 +37375,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.24 +20457,,,,,,,,1,0.23 +29159,,,,,,,,0, +35195,,,,,,,,1,0.45 +69343,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.24 +42905,,,,,,,,0, +48239,95.0,10.0,9.0,10.0,10.0,10.0,10.0,34,4.29 +18933,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +19436,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.24 +20008,,,,,,,,0, +46201,95.0,9.0,10.0,10.0,10.0,10.0,10.0,35,4.47 +46700,97.0,10.0,10.0,10.0,10.0,9.0,10.0,6,0.94 +22595,,,,,,,,0, +15163,93.0,10.0,9.0,10.0,10.0,10.0,10.0,16,2.15 +51196,88.0,8.0,10.0,9.0,9.0,10.0,10.0,6,0.87 +46241,100.0,10.0,8.0,10.0,10.0,8.0,10.0,1,0.19 +44188,89.0,10.0,9.0,10.0,10.0,9.0,9.0,14,2.09 +22504,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.14 +69468,,,,,,,,0, +60751,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.85 +6745,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.26 +17301,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.65 +32856,,,,,,,,0, +68769,96.0,10.0,10.0,10.0,10.0,10.0,9.0,6,0.83 +45066,,,,,,,,0, +18805,89.0,9.0,8.0,10.0,10.0,10.0,9.0,12,1.49 +30486,,,,,,,,0, +51552,98.0,10.0,10.0,10.0,10.0,10.0,10.0,38,5.48 +35975,85.0,9.0,9.0,9.0,9.0,10.0,9.0,12,1.57 +41151,91.0,8.0,9.0,10.0,9.0,10.0,9.0,9,1.26 +18292,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.13 +56223,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.63 +39819,100.0,10.0,10.0,8.0,10.0,10.0,10.0,2,0.9 +52757,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.15 +54020,,,,,,,,0, +7123,90.0,9.0,10.0,10.0,10.0,10.0,10.0,6,0.91 +19418,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.13 +23132,76.0,9.0,9.0,8.0,9.0,10.0,9.0,9,2.27 +3011,97.0,10.0,10.0,10.0,10.0,10.0,10.0,44,5.64 +61328,,,,,,,,0, +77088,91.0,9.0,8.0,10.0,9.0,10.0,9.0,19,2.79 +19698,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.15 +16026,83.0,9.0,8.0,9.0,9.0,9.0,9.0,14,3.11 +40018,87.0,9.0,8.0,9.0,9.0,9.0,6.0,3,0.56 +40698,95.0,10.0,10.0,10.0,10.0,9.0,10.0,13,2.14 +5849,97.0,10.0,9.0,10.0,10.0,10.0,9.0,18,2.61 +41911,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.18 +36326,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.38 +16777,86.0,9.0,9.0,8.0,9.0,10.0,9.0,23,2.97 +38870,87.0,9.0,9.0,9.0,9.0,10.0,9.0,27,3.49 +12810,100.0,10.0,10.0,9.0,10.0,10.0,10.0,12,1.59 +47441,90.0,9.0,10.0,9.0,8.0,10.0,9.0,30,3.98 +59206,,,,,,,,0, +9577,91.0,9.0,10.0,9.0,8.0,10.0,9.0,30,3.86 +60132,99.0,10.0,10.0,10.0,10.0,10.0,10.0,21,2.93 +37043,84.0,10.0,9.0,9.0,9.0,10.0,9.0,11,1.44 +32553,100.0,10.0,10.0,10.0,10.0,10.0,10.0,24,3.19 +2766,90.0,9.0,10.0,9.0,10.0,10.0,9.0,2,0.27 +5396,,,,,,,,1,0.13 +40664,94.0,10.0,10.0,10.0,10.0,10.0,9.0,25,3.25 +66573,94.0,9.0,9.0,9.0,8.0,10.0,10.0,19,2.71 +54230,95.0,10.0,10.0,10.0,10.0,10.0,9.0,21,3.01 +59211,89.0,10.0,9.0,9.0,10.0,10.0,9.0,7,2.53 +53880,,,,,,,,0, +51567,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.67 +48181,87.0,10.0,9.0,10.0,10.0,10.0,9.0,19,2.48 +8703,100.0,9.0,10.0,8.0,10.0,10.0,8.0,3,0.6 +50310,78.0,9.0,8.0,8.0,8.0,10.0,8.0,13,1.85 +61133,100.0,10.0,10.0,9.0,10.0,10.0,10.0,6,1.12 +31012,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.8 +4791,93.0,10.0,9.0,10.0,10.0,10.0,10.0,9,2.35 +68941,90.0,10.0,9.0,10.0,10.0,10.0,9.0,31,3.89 +43569,81.0,8.0,8.0,8.0,9.0,10.0,8.0,14,1.9 +40109,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.36 +7245,97.0,10.0,10.0,10.0,10.0,10.0,10.0,23,3.4 +44003,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.42 +32717,97.0,10.0,9.0,10.0,10.0,10.0,10.0,17,2.37 +63865,100.0,10.0,10.0,10.0,10.0,9.0,10.0,13,1.81 +5236,80.0,9.0,7.0,9.0,10.0,8.0,9.0,3,2.37 +39675,89.0,9.0,9.0,9.0,10.0,10.0,7.0,7,0.96 +62904,,,,,,,,0, +33960,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,0.91 +4397,98.0,10.0,10.0,10.0,10.0,10.0,9.0,8,1.06 +57952,,,,,,,,0, +17848,97.0,10.0,9.0,10.0,10.0,10.0,10.0,13,1.75 +50180,88.0,10.0,10.0,10.0,9.0,9.0,9.0,5,0.68 +49353,97.0,10.0,10.0,9.0,10.0,10.0,9.0,18,2.71 +53963,,,,,,,,0, +25253,99.0,10.0,10.0,10.0,10.0,9.0,10.0,28,3.89 +59116,73.0,10.0,10.0,10.0,7.0,9.0,8.0,3,0.39 +36169,100.0,10.0,9.0,9.0,10.0,10.0,10.0,2,0.26 +49370,96.0,10.0,9.0,10.0,10.0,10.0,10.0,10,1.35 +20505,68.0,8.0,7.0,7.0,8.0,9.0,8.0,8,1.06 +3837,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.33 +18552,88.0,9.0,9.0,9.0,9.0,9.0,9.0,29,4.1 +68502,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.75 +51426,92.0,10.0,10.0,9.0,10.0,10.0,9.0,5,0.8 +6887,80.0,8.0,8.0,8.0,8.0,10.0,10.0,1,0.48 +12712,87.0,8.0,7.0,9.0,9.0,9.0,8.0,6,1.12 +3859,100.0,10.0,6.0,10.0,10.0,10.0,10.0,2,0.74 +35318,,,,,,,,0, +69955,97.0,10.0,10.0,10.0,10.0,9.0,10.0,15,2.24 +74705,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.7 +1415,75.0,8.0,9.0,9.0,9.0,9.0,8.0,15,2.08 +66269,100.0,10.0,10.0,10.0,10.0,10.0,10.0,19,3.54 +65715,,,,,,,,0, +43100,,,,,,,,1,0.13 +35248,100.0,10.0,10.0,9.0,10.0,10.0,10.0,2,0.3 +29840,,,,,,,,0, +43141,100.0,10.0,10.0,9.0,10.0,9.0,9.0,4,0.67 +34750,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.46 +9588,95.0,10.0,9.0,10.0,10.0,10.0,9.0,4,0.57 +72144,94.0,10.0,9.0,10.0,10.0,9.0,9.0,32,4.3 +47628,90.0,10.0,9.0,10.0,9.0,10.0,9.0,2,0.36 +46275,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.15 +5754,95.0,10.0,10.0,9.0,10.0,10.0,9.0,23,3.17 +59941,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.56 +72821,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.27 +66874,80.0,10.0,6.0,8.0,10.0,8.0,8.0,1,0.24 +36098,88.0,9.0,9.0,10.0,10.0,10.0,9.0,19,2.71 +31714,89.0,8.0,9.0,9.0,9.0,10.0,9.0,38,5.3 +18654,97.0,10.0,9.0,10.0,10.0,9.0,9.0,8,1.07 +35213,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.57 +27069,93.0,9.0,10.0,10.0,10.0,9.0,9.0,14,2.06 +65785,,,,,,,,1,0.14 +45333,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,1.57 +38053,100.0,10.0,9.0,9.0,10.0,10.0,10.0,4,0.69 +65430,60.0,5.0,6.0,7.0,7.0,6.0,6.0,3,0.46 +13837,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.75 +64810,70.0,9.0,8.0,9.0,10.0,10.0,10.0,2,0.8 +49861,40.0,6.0,6.0,10.0,8.0,10.0,6.0,1,0.17 +3644,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.8 +47842,87.0,8.0,9.0,9.0,9.0,10.0,9.0,12,1.72 +23922,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.07 +29596,96.0,10.0,10.0,10.0,10.0,10.0,8.0,5,0.69 +12021,80.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.29 +65748,,,,,,,,0, +2798,,,,,,,,1,0.14 +32688,87.0,9.0,9.0,10.0,9.0,9.0,9.0,17,2.25 +25581,80.0,10.0,10.0,6.0,10.0,6.0,6.0,2,0.34 +48271,,,,,,,,0, +34385,80.0,8.0,7.0,10.0,10.0,7.0,7.0,2,0.97 +7884,80.0,8.0,8.0,9.0,9.0,10.0,10.0,2,0.3 +7392,80.0,10.0,8.0,10.0,10.0,10.0,8.0,2,0.3 +16284,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.76 +17001,,,,,,,,0, +5474,88.0,10.0,9.0,9.0,9.0,10.0,9.0,20,2.9 +61844,100.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.56 +53777,,,,,,,,0, +43995,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.69 +17564,,,,,,,,0, +36285,91.0,10.0,10.0,10.0,10.0,10.0,9.0,17,2.54 +33857,,,,,,,,0, +52443,,,,,,,,0, +62338,89.0,10.0,9.0,10.0,10.0,10.0,9.0,8,1.07 +68061,99.0,10.0,10.0,10.0,10.0,9.0,10.0,17,2.98 +75201,90.0,10.0,9.0,9.0,10.0,9.0,10.0,3,0.42 +6599,100.0,10.0,10.0,9.0,9.0,10.0,9.0,2,0.39 +63043,,,,,,,,0, +58087,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,1.67 +12597,,,,,,,,0, +50478,95.0,10.0,9.0,10.0,10.0,10.0,9.0,22,5.32 +38608,85.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.54 +24672,96.0,10.0,10.0,10.0,10.0,10.0,10.0,15,2.25 +8331,93.0,9.0,9.0,10.0,10.0,10.0,10.0,4,0.57 +71449,97.0,10.0,9.0,9.0,10.0,9.0,10.0,14,2.01 +71272,93.0,9.0,9.0,10.0,10.0,10.0,9.0,9,1.69 +26672,94.0,10.0,9.0,10.0,10.0,9.0,9.0,26,3.61 +24308,,,,,,,,0, +76004,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.14 +57740,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,1.51 +53940,97.0,10.0,9.0,10.0,10.0,9.0,9.0,6,0.87 +68407,100.0,10.0,10.0,10.0,8.0,10.0,10.0,1,0.14 +54518,,,,,,,,0, +36382,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.59 +34731,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.79 +61228,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.3 +43806,98.0,10.0,10.0,10.0,10.0,9.0,9.0,21,2.92 +73871,,,,,,,,0, +26522,100.0,9.0,10.0,10.0,10.0,10.0,10.0,2,0.3 +27207,97.0,10.0,10.0,10.0,10.0,10.0,9.0,14,2.02 +28501,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.14 +73364,98.0,9.0,9.0,10.0,10.0,10.0,10.0,10,1.39 +61651,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.82 +47999,94.0,10.0,10.0,10.0,10.0,10.0,10.0,14,2.4 +37937,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.85 +17520,100.0,10.0,10.0,10.0,10.0,10.0,10.0,20,2.86 +63615,95.0,10.0,8.0,10.0,10.0,9.0,10.0,4,0.54 +58789,88.0,9.0,8.0,10.0,9.0,9.0,9.0,5,0.73 +14042,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,1.65 +8968,60.0,6.0,8.0,10.0,10.0,10.0,6.0,1,1.0 +35964,98.0,10.0,9.0,10.0,10.0,10.0,10.0,27,3.72 +60783,83.0,8.0,8.0,10.0,7.0,10.0,7.0,6,1.03 +28101,,,,,,,,0, +36410,,,,,,,,0, +73215,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.83 +10239,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.56 +19715,94.0,10.0,9.0,10.0,10.0,10.0,10.0,18,2.42 +57760,95.0,10.0,9.0,10.0,10.0,9.0,10.0,42,6.18 +9766,40.0,8.0,8.0,10.0,10.0,10.0,4.0,2,0.36 +19279,,,,,,,,0, +74591,,,,,,,,0, +3597,,,,,,,,0, +61926,,,,,,,,0, +16477,95.0,10.0,10.0,10.0,10.0,10.0,9.0,13,1.87 +73057,,,,,,,,1,0.19 +130,20.0,,,2.0,2.0,,,1,0.13 +17738,50.0,5.0,5.0,6.0,5.0,6.0,5.0,2,0.38 +32592,96.0,9.0,10.0,10.0,10.0,9.0,10.0,9,1.34 +52720,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.26 +19131,83.0,9.0,8.0,9.0,9.0,8.0,8.0,16,2.2 +66076,90.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.4 +29328,91.0,10.0,10.0,9.0,10.0,10.0,9.0,19,2.58 +33548,,,,,,,,0, +65935,,,,,,,,1,0.16 +57563,60.0,8.0,4.0,8.0,8.0,6.0,6.0,1,0.16 +59946,100.0,10.0,10.0,10.0,10.0,9.0,9.0,21,2.92 +25021,92.0,9.0,10.0,9.0,10.0,9.0,9.0,17,3.15 +63684,94.0,10.0,10.0,9.0,10.0,9.0,9.0,27,4.35 +17524,89.0,9.0,9.0,10.0,10.0,10.0,10.0,34,5.7 +1105,,,,,,,,0, +65456,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +25997,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.33 +30112,99.0,10.0,10.0,10.0,10.0,9.0,10.0,38,5.51 +48389,99.0,10.0,9.0,10.0,10.0,10.0,10.0,33,4.4 +4289,100.0,9.0,9.0,9.0,9.0,10.0,9.0,2,0.3 +47267,95.0,9.0,9.0,9.0,9.0,9.0,9.0,8,1.15 +18251,93.0,10.0,10.0,9.0,10.0,9.0,9.0,6,1.19 +67565,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.15 +6438,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.29 +71598,80.0,9.0,9.0,9.0,9.0,9.0,9.0,12,1.78 +37981,96.0,9.0,10.0,10.0,9.0,10.0,10.0,5,0.72 +15751,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.47 +43321,100.0,9.0,9.0,10.0,9.0,9.0,10.0,3,0.42 +56132,97.0,10.0,9.0,10.0,10.0,10.0,9.0,14,2.07 +10676,86.0,9.0,9.0,9.0,9.0,9.0,9.0,18,2.51 +63689,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.15 +33239,91.0,9.0,9.0,9.0,10.0,10.0,9.0,13,2.17 +55759,85.0,9.0,9.0,9.0,9.0,9.0,9.0,46,6.54 +62618,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.18 +53284,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.25 +65617,,,,,,,,0, +39284,100.0,9.0,9.0,10.0,10.0,10.0,10.0,3,0.47 +43976,67.0,9.0,5.0,8.0,8.0,10.0,7.0,4,0.63 +52528,87.0,10.0,8.0,9.0,9.0,10.0,9.0,3,0.44 +50469,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.67 +75793,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.31 +44706,93.0,10.0,10.0,10.0,10.0,9.0,10.0,7,1.05 +63642,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.51 +59144,80.0,8.0,7.0,9.0,9.0,8.0,9.0,2,0.31 +7287,,,,,,,,0, +71524,,,,,,,,0, +76796,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.94 +24885,90.0,9.0,10.0,8.0,10.0,8.0,9.0,2,0.31 +67137,,,,,,,,0, +68323,87.0,10.0,7.0,10.0,9.0,8.0,8.0,3,0.56 +16770,97.0,9.0,9.0,10.0,10.0,10.0,10.0,8,1.26 +52596,96.0,10.0,9.0,10.0,10.0,10.0,9.0,5,0.81 +28010,100.0,10.0,10.0,10.0,10.0,8.0,9.0,5,0.74 +66677,88.0,9.0,9.0,10.0,10.0,8.0,9.0,24,3.44 +39015,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.51 +38786,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.28 +30249,95.0,9.0,9.0,10.0,10.0,9.0,10.0,4,0.76 +33851,90.0,9.0,8.0,9.0,9.0,10.0,9.0,14,2.07 +11569,83.0,9.0,9.0,10.0,9.0,9.0,8.0,7,1.21 +58970,100.0,10.0,10.0,10.0,8.0,10.0,8.0,1,0.24 +74747,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.41 +1053,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,3.18 +29897,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +41142,87.0,9.0,8.0,10.0,10.0,9.0,9.0,3,0.56 +72956,95.0,10.0,10.0,10.0,9.0,10.0,10.0,4,0.63 +35514,93.0,9.0,9.0,10.0,10.0,9.0,9.0,9,1.35 +73548,,,,,,,,0, +44293,95.0,10.0,10.0,10.0,10.0,9.0,10.0,45,6.22 +67095,93.0,9.0,10.0,10.0,8.0,10.0,9.0,6,0.85 +70665,95.0,10.0,10.0,10.0,10.0,10.0,9.0,7,1.09 +50706,,,,,,,,0, +30305,,,,,,,,0, +36058,96.0,10.0,10.0,9.0,10.0,9.0,9.0,16,2.89 +33126,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.55 +55728,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,2.45 +39194,67.0,7.0,6.0,10.0,9.0,10.0,6.0,4,0.71 +45212,,,,,,,,0, +62483,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.5 +13756,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.28 +1320,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.62 +2225,90.0,10.0,9.0,9.0,10.0,10.0,9.0,8,1.23 +27125,97.0,10.0,10.0,10.0,10.0,10.0,10.0,28,4.12 +38073,84.0,8.0,8.0,8.0,8.0,8.0,8.0,5,0.69 +16154,94.0,10.0,9.0,10.0,10.0,10.0,10.0,7,1.35 +8490,95.0,10.0,10.0,10.0,10.0,10.0,9.0,9,1.29 +8375,,,,,,,,0, +38719,98.0,9.0,10.0,10.0,10.0,10.0,10.0,9,1.48 +25754,70.0,8.0,8.0,10.0,8.0,10.0,8.0,4,0.62 +57254,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.57 +6662,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.34 +18234,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.23 +55624,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.21 +62009,,,,,,,,0, +29985,94.0,9.0,10.0,9.0,9.0,9.0,9.0,7,0.97 +57350,94.0,10.0,9.0,10.0,10.0,9.0,9.0,7,1.3 +38380,97.0,10.0,10.0,10.0,10.0,9.0,10.0,15,2.74 +62635,,,,,,,,0, +25987,,,,,,,,1, +45924,,,,,,,,0, +48060,,,,,,,,0, +60039,96.0,10.0,10.0,10.0,10.0,9.0,10.0,18,2.5 +18382,85.0,9.0,9.0,9.0,9.0,9.0,9.0,19,3.1 +50858,92.0,9.0,8.0,9.0,10.0,10.0,9.0,22,3.16 +65228,,,,,,,,0, +5666,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,2.79 +48752,92.0,8.0,8.0,10.0,10.0,9.0,9.0,5,0.74 +43398,99.0,10.0,10.0,10.0,10.0,10.0,10.0,29,4.03 +73723,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,1.81 +10797,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.71 +48958,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.16 +71140,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,1.03 +34601,,,,,,,,0, +21237,95.0,10.0,9.0,9.0,10.0,9.0,9.0,23,3.3 +73038,,,,,,,,1,0.55 +75289,90.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.83 +15872,,,,,,,,0, +64324,86.0,9.0,9.0,9.0,9.0,9.0,9.0,7,1.24 +4278,93.0,10.0,8.0,9.0,9.0,8.0,9.0,4,0.69 +7643,,,,,,,,0, +76950,,,,,,,,0, +47342,,,,,,,,0, +65105,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.29 +34787,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.81 +46403,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.5 +12570,88.0,9.0,10.0,9.0,9.0,9.0,9.0,5,0.71 +1042,87.0,9.0,9.0,9.0,10.0,9.0,9.0,33,4.74 +30626,84.0,9.0,9.0,9.0,9.0,10.0,9.0,31,4.47 +35286,87.0,9.0,9.0,9.0,9.0,9.0,9.0,24,3.46 +68574,,,,,,,,0, +26142,98.0,10.0,10.0,10.0,10.0,10.0,10.0,56,8.48 +54573,80.0,9.0,8.0,9.0,10.0,9.0,9.0,6,0.85 +24737,95.0,9.0,10.0,9.0,10.0,10.0,9.0,15,2.15 +35246,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.25 +59560,,,,,,,,0, +54871,60.0,2.0,8.0,8.0,10.0,10.0,8.0,1,0.21 +60372,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.48 +55192,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.55 +35602,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,1.05 +48749,60.0,5.0,4.0,9.0,8.0,7.0,6.0,2,0.4 +32886,92.0,10.0,9.0,10.0,9.0,9.0,9.0,10,1.6 +53349,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.94 +17254,96.0,10.0,9.0,10.0,10.0,10.0,10.0,16,2.35 +6063,90.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.41 +45210,100.0,10.0,9.0,10.0,10.0,10.0,10.0,5,1.44 +32236,93.0,10.0,9.0,10.0,10.0,10.0,10.0,7,1.05 +52964,99.0,10.0,10.0,10.0,10.0,10.0,10.0,29,4.16 +23641,,,,,,,,0, +61513,,,,,,,,0, +9702,,,,,,,,1,0.34 +53252,100.0,10.0,9.0,10.0,10.0,10.0,9.0,4,0.92 +59220,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,1.01 +28695,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.78 +56533,,,,,,,,0, +3616,87.0,9.0,8.0,10.0,10.0,10.0,9.0,22,3.35 +73840,,,,,,,,1,0.67 +12875,,,,,,,,0, +8508,91.0,9.0,9.0,10.0,10.0,9.0,10.0,7,1.4 +74338,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.49 +54668,96.0,10.0,8.0,10.0,10.0,9.0,9.0,10,1.42 +1795,,,,,,,,0, +36370,60.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.28 +28135,90.0,10.0,8.0,9.0,10.0,10.0,10.0,2,0.36 +4911,68.0,8.0,8.0,8.0,9.0,8.0,8.0,8,1.3 +37301,99.0,10.0,10.0,10.0,10.0,10.0,10.0,32,4.82 +30522,85.0,9.0,9.0,10.0,10.0,10.0,9.0,49,6.87 +66205,,,,,,,,0, +41859,96.0,10.0,10.0,10.0,10.0,10.0,10.0,41,6.09 +17203,95.0,9.0,10.0,10.0,10.0,9.0,10.0,12,2.09 +68757,96.0,10.0,10.0,10.0,10.0,10.0,9.0,24,3.67 +23786,96.0,9.0,9.0,10.0,10.0,10.0,9.0,9,1.67 +11048,97.0,10.0,10.0,10.0,10.0,9.0,9.0,6,0.95 +5612,96.0,10.0,8.0,10.0,10.0,9.0,8.0,5,1.15 +61455,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,2.02 +21153,67.0,7.0,7.0,7.0,7.0,7.0,7.0,3,1.34 +51078,100.0,10.0,9.0,9.0,9.0,9.0,9.0,3,0.48 +13342,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.5 +10647,85.0,9.0,9.0,9.0,9.0,9.0,9.0,12,2.05 +58786,89.0,10.0,7.0,10.0,10.0,10.0,9.0,9,1.44 +62213,90.0,10.0,9.0,9.0,8.0,10.0,9.0,2,0.33 +74502,93.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.68 +41018,80.0,10.0,10.0,8.0,10.0,10.0,10.0,1,0.18 +42890,92.0,10.0,10.0,9.0,10.0,10.0,9.0,5,1.15 +63399,89.0,9.0,9.0,9.0,9.0,8.0,8.0,9,1.34 +55088,98.0,10.0,10.0,9.0,10.0,10.0,10.0,13,2.27 +21809,85.0,8.0,10.0,9.0,9.0,8.0,8.0,8,1.42 +49423,89.0,10.0,8.0,9.0,10.0,10.0,9.0,17,2.93 +58153,93.0,9.0,9.0,10.0,10.0,9.0,9.0,51,7.97 +74978,91.0,10.0,8.0,10.0,10.0,10.0,10.0,7,1.08 +73955,,,,,,,,0, +6303,,,,,,,,0, +24607,99.0,10.0,10.0,10.0,10.0,10.0,10.0,26,4.29 +39530,100.0,10.0,8.0,10.0,10.0,10.0,10.0,6,2.95 +12122,96.0,10.0,10.0,10.0,10.0,9.0,9.0,22,3.27 +4199,95.0,10.0,10.0,10.0,10.0,9.0,10.0,26,3.71 +44650,93.0,9.0,9.0,9.0,10.0,9.0,8.0,4,0.62 +31161,70.0,8.0,7.0,9.0,9.0,9.0,8.0,4,0.62 +3375,90.0,8.0,8.0,10.0,10.0,10.0,10.0,2,0.48 +12380,89.0,10.0,9.0,8.0,9.0,10.0,9.0,13,2.07 +72775,80.0,9.0,9.0,9.0,10.0,9.0,8.0,6,1.08 +43669,,,,,,,,0, +1869,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.37 +15138,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.47 +29281,96.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.96 +5302,90.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.62 +4303,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,0.28 +31223,100.0,10.0,10.0,6.0,10.0,8.0,8.0,1,0.77 +68783,,,,,,,,0, +18882,92.0,10.0,9.0,10.0,10.0,10.0,9.0,11,2.26 +31818,93.0,9.0,9.0,10.0,9.0,10.0,10.0,6,0.9 +74717,60.0,4.0,6.0,10.0,10.0,10.0,4.0,1,0.22 +43589,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.16 +57778,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.27 +10324,,,,,,,,0, +69683,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.18 +74400,90.0,10.0,9.0,10.0,10.0,10.0,10.0,8,1.36 +52187,60.0,7.0,5.0,10.0,9.0,8.0,4.0,2,0.75 +8568,,,,,,,,0, +32920,100.0,8.0,10.0,8.0,8.0,10.0,10.0,1,0.17 +43887,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +63931,100.0,10.0,9.0,10.0,10.0,9.0,10.0,4,0.76 +74426,,,,,,,,0, +63891,90.0,10.0,7.0,10.0,9.0,10.0,9.0,4,0.63 +25832,,,,,,,,0, +34734,,,,,,,,0, +65977,,,,,,,,1,1.0 +58766,90.0,9.0,9.0,10.0,10.0,8.0,10.0,8,1.28 +33674,94.0,10.0,9.0,10.0,10.0,10.0,9.0,10,1.58 +73172,100.0,10.0,7.0,10.0,10.0,10.0,10.0,4,0.96 +38458,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.24 +54643,95.0,9.0,9.0,10.0,9.0,9.0,9.0,15,2.65 +59091,93.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.28 +31760,95.0,9.0,7.0,8.0,10.0,9.0,9.0,4,0.9 +30208,,,,,,,,0, +28261,95.0,10.0,10.0,10.0,10.0,9.0,9.0,23,3.38 +50707,89.0,9.0,8.0,9.0,9.0,9.0,9.0,7,1.19 +46941,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.25 +75811,100.0,6.0,6.0,10.0,10.0,10.0,6.0,1,0.88 +13562,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.92 +11425,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.47 +21182,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.47 +55913,98.0,10.0,10.0,10.0,10.0,9.0,9.0,8,1.26 +54628,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,1.82 +32166,71.0,8.0,8.0,8.0,9.0,9.0,7.0,11,1.76 +71541,70.0,10.0,9.0,10.0,8.0,9.0,9.0,7,1.27 +76524,,,,,,,,0, +39419,97.0,10.0,10.0,10.0,10.0,9.0,9.0,36,5.81 +43442,90.0,10.0,9.0,9.0,9.0,9.0,9.0,37,5.44 +20626,91.0,9.0,9.0,9.0,9.0,9.0,9.0,9,1.62 +61681,96.0,10.0,10.0,10.0,10.0,9.0,9.0,11,1.75 +66458,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +5488,100.0,10.0,10.0,8.0,10.0,10.0,10.0,2,0.36 +1153,100.0,6.0,10.0,10.0,10.0,6.0,6.0,1,0.47 +63517,,,,,,,,0, +34191,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.3 +50926,87.0,9.0,9.0,9.0,10.0,9.0,9.0,3,0.53 +31799,,,,,,,,0, +33904,,,,,,,,0, +11012,,,,,,,,0, +13044,100.0,10.0,9.0,10.0,10.0,10.0,9.0,3,1.5 +9767,,,,,,,,0, +68054,90.0,10.0,9.0,10.0,9.0,10.0,8.0,2,0.41 +41888,,,,,,,,0, +30205,100.0,9.0,9.0,9.0,9.0,10.0,10.0,3,0.52 +62996,100.0,10.0,10.0,10.0,10.0,10.0,10.0,31,4.79 +50609,40.0,3.0,3.0,6.0,6.0,6.0,3.0,2,0.36 +32430,78.0,8.0,8.0,9.0,9.0,9.0,9.0,9,1.62 +73516,60.0,6.0,6.0,8.0,8.0,8.0,4.0,1,0.56 +65563,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.63 +25897,100.0,10.0,10.0,10.0,10.0,10.0,10.0,33,4.88 +57544,,,,,,,,1,1.0 +67210,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.54 +35105,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.27 +26715,60.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.15 +76383,87.0,9.0,9.0,10.0,10.0,10.0,9.0,19,2.84 +58853,,,,,,,,0, +58090,,,,,,,,1,0.15 +32671,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.18 +23825,,,,,,,,0, +44064,96.0,10.0,10.0,10.0,10.0,10.0,10.0,14,2.76 +43912,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.71 +68071,83.0,9.0,9.0,9.0,9.0,9.0,8.0,6,1.15 +74424,86.0,8.0,7.0,10.0,9.0,10.0,9.0,8,1.38 +8559,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.51 +2373,,,,,,,,0, +2764,,,,,,,,0, +69322,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,2.21 +61048,90.0,9.0,9.0,9.0,8.0,10.0,9.0,35,5.12 +14755,87.0,7.0,9.0,10.0,7.0,9.0,9.0,4,1.33 +48464,92.0,10.0,9.0,10.0,10.0,10.0,9.0,5,0.86 +76510,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,3.23 +70699,80.0,9.0,7.0,10.0,10.0,8.0,9.0,5,0.75 +72355,20.0,4.0,2.0,4.0,4.0,6.0,2.0,1,1.0 +56713,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.73 +72019,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.46 +64115,,,,,,,,0, +7439,60.0,8.0,2.0,2.0,6.0,10.0,6.0,1,0.46 +60383,,,,,,,,1, +46336,93.0,9.0,10.0,9.0,8.0,9.0,9.0,3,1.2 +57470,60.0,8.0,4.0,10.0,10.0,10.0,6.0,1,1.0 +33255,,,,,,,,0, +64937,,,,,,,,0, +33927,80.0,8.0,10.0,10.0,10.0,10.0,10.0,3,0.55 +64560,20.0,2.0,2.0,2.0,2.0,2.0,2.0,1,1.0 +4254,90.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.38 +5918,,,,,,,,0, +35302,60.0,8.0,10.0,8.0,8.0,8.0,6.0,1,0.16 +76069,93.0,9.0,9.0,10.0,10.0,10.0,10.0,3,0.58 +4677,91.0,10.0,9.0,10.0,10.0,10.0,10.0,37,5.78 +20250,93.0,10.0,9.0,9.0,10.0,10.0,10.0,7,1.08 +32202,80.0,8.0,8.0,8.0,8.0,6.0,6.0,1,0.18 +15700,,,,,,,,0, +60091,90.0,10.0,10.0,9.0,10.0,10.0,8.0,2,0.38 +42448,93.0,10.0,10.0,10.0,10.0,10.0,9.0,42,6.49 +4595,91.0,10.0,9.0,9.0,10.0,10.0,9.0,7,1.11 +8005,98.0,10.0,10.0,10.0,10.0,9.0,10.0,10,1.88 +17335,90.0,7.0,8.0,7.0,9.0,10.0,9.0,3,0.48 +408,98.0,10.0,10.0,10.0,10.0,9.0,10.0,12,1.9 +21625,80.0,8.0,10.0,10.0,10.0,8.0,10.0,1,0.23 +40454,97.0,10.0,10.0,10.0,10.0,9.0,10.0,31,5.74 +60058,90.0,9.0,10.0,9.0,9.0,9.0,10.0,2,0.6 +17686,80.0,7.0,8.0,9.0,9.0,10.0,8.0,7,1.21 +73754,,,,,,,,0, +71908,90.0,9.0,9.0,10.0,9.0,10.0,8.0,11,1.83 +6288,,,,,,,,1,0.15 +61385,73.0,10.0,10.0,10.0,10.0,8.0,7.0,3,0.91 +71987,,,,,,,,0, +31936,96.0,10.0,10.0,10.0,10.0,9.0,10.0,11,1.82 +58706,88.0,9.0,9.0,10.0,9.0,9.0,9.0,10,2.19 +15879,98.0,10.0,10.0,10.0,10.0,10.0,9.0,12,2.42 +63779,,,,,,,,0, +31874,,,,,,,,0, +71939,100.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.63 +29086,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.67 +76081,60.0,6.0,6.0,10.0,10.0,10.0,8.0,1,0.42 +29995,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.65 +8267,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,0.46 +21319,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,2.31 +37653,,,,,,,,0, +20421,91.0,9.0,9.0,10.0,10.0,10.0,9.0,9,1.34 +4889,,,,,,,,2,0.37 +32424,89.0,9.0,9.0,10.0,10.0,10.0,9.0,45,6.89 +66441,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.17 +64814,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.47 +67245,89.0,9.0,9.0,10.0,10.0,10.0,9.0,13,3.25 +18840,,,,,,,,0, +73017,,,,,,,,0, +51446,50.0,6.0,6.0,6.0,6.0,6.0,6.0,2,0.33 +61568,76.0,7.0,7.0,8.0,9.0,9.0,8.0,11,1.74 +2269,92.0,9.0,10.0,9.0,10.0,8.0,9.0,13,2.28 +34174,,,,,,,,0, +22144,95.0,10.0,9.0,10.0,10.0,10.0,9.0,22,3.84 +35540,91.0,9.0,9.0,10.0,10.0,10.0,9.0,12,1.91 +11785,,,,,,,,0, +17342,93.0,10.0,10.0,9.0,10.0,9.0,9.0,3,0.71 +53373,92.0,9.0,9.0,10.0,10.0,10.0,9.0,15,2.39 +1410,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.55 +73324,,,,,,,,1,0.15 +42248,90.0,9.0,9.0,9.0,10.0,9.0,9.0,14,2.21 +67799,100.0,9.0,10.0,10.0,10.0,10.0,9.0,4,0.71 +74273,65.0,8.0,6.0,10.0,6.0,10.0,7.0,4,0.79 +67334,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.04 +11603,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,2.34 +37234,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,0.98 +26245,96.0,10.0,9.0,10.0,10.0,9.0,10.0,27,4.22 +70988,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.62 +6165,89.0,10.0,9.0,10.0,10.0,9.0,9.0,11,1.95 +70656,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,0.59 +4460,90.0,9.0,9.0,9.0,10.0,10.0,9.0,2,0.34 +55564,99.0,10.0,10.0,10.0,10.0,9.0,10.0,21,3.3 +9850,90.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.34 +63185,,,,,,,,0, +30508,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.99 +67053,80.0,9.0,9.0,10.0,10.0,10.0,8.0,2,0.41 +1669,,,,,,,,0, +66113,,,,,,,,0, +45974,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,3.67 +50074,,,,,,,,0, +71969,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.35 +37153,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,1.1 +22113,,,,,,,,0, +50667,80.0,9.0,8.0,9.0,9.0,9.0,8.0,14,2.76 +46512,80.0,9.0,7.0,10.0,10.0,7.0,8.0,2,0.3 +15833,,,,,,,,0, +12641,96.0,10.0,10.0,9.0,9.0,10.0,10.0,9,1.49 +42712,,,,,,,,0, +61096,67.0,8.0,7.0,9.0,8.0,10.0,7.0,10,1.51 +69413,,,,,,,,0, +61874,90.0,10.0,9.0,9.0,9.0,10.0,10.0,10,1.55 +60084,90.0,8.0,8.0,8.0,9.0,10.0,9.0,2,0.41 +47243,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,1.18 +23574,,,,,,,,0, +50451,,,,,,,,0, +63895,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.55 +76040,76.0,8.0,8.0,8.0,7.0,9.0,8.0,12,1.87 +7549,96.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.93 +11727,20.0,2.0,6.0,6.0,2.0,8.0,2.0,1,0.2 +74453,91.0,10.0,9.0,10.0,10.0,10.0,9.0,52,7.96 +57978,,,,,,,,1,0.16 +31466,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.56 +42638,,,,,,,,1,0.24 +30475,40.0,10.0,4.0,8.0,8.0,10.0,8.0,1,1.0 +52316,89.0,9.0,9.0,9.0,10.0,9.0,9.0,7,1.26 +35563,94.0,10.0,10.0,10.0,10.0,10.0,10.0,13,2.0 +57024,97.0,10.0,10.0,10.0,10.0,10.0,10.0,40,6.06 +67795,93.0,9.0,9.0,8.0,9.0,9.0,10.0,6,0.96 +47636,,,,,,,,0, +22150,98.0,10.0,10.0,10.0,10.0,10.0,10.0,40,6.45 +42345,,,,,,,,2,0.34 +22180,90.0,10.0,8.0,10.0,10.0,10.0,10.0,5,0.98 +21606,,,,,,,,0, +8785,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.39 +46196,80.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.2 +38022,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.6 +62453,87.0,9.0,8.0,10.0,10.0,10.0,9.0,23,4.34 +40815,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.68 +64429,91.0,10.0,10.0,10.0,9.0,9.0,9.0,14,2.41 +22102,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.3 +37205,92.0,10.0,10.0,9.0,9.0,8.0,9.0,5,1.29 +47907,90.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.85 +76512,98.0,10.0,10.0,10.0,10.0,10.0,10.0,14,2.15 +12742,98.0,10.0,10.0,10.0,10.0,10.0,10.0,53,8.46 +48556,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.27 +19495,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.34 +63995,,,,,,,,0, +45658,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.93 +13488,92.0,10.0,10.0,9.0,10.0,10.0,9.0,5,1.72 +46143,70.0,6.0,8.0,7.0,9.0,7.0,8.0,2,0.4 +40319,,,,,,,,0, +40772,,,,,,,,1,0.17 +24257,,,,,,,,0, +56439,96.0,10.0,10.0,10.0,10.0,10.0,10.0,10,1.6 +60368,93.0,10.0,9.0,9.0,10.0,9.0,10.0,5,0.83 +22969,80.0,8.0,7.0,8.0,9.0,8.0,8.0,5,0.94 +34671,,,,,,,,0, +72482,77.0,9.0,8.0,10.0,9.0,9.0,9.0,23,3.52 +69540,95.0,10.0,10.0,10.0,10.0,10.0,9.0,12,1.95 +75717,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.18 +51768,,,,,,,,0, +22318,98.0,10.0,10.0,10.0,10.0,10.0,10.0,11,3.88 +37334,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.72 +33540,100.0,10.0,10.0,10.0,10.0,10.0,8.0,6,1.36 +59048,100.0,10.0,10.0,10.0,10.0,9.0,10.0,8,3.75 +19423,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,1.36 +50449,95.0,10.0,9.0,9.0,10.0,9.0,9.0,11,1.8 +607,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.25 +43449,98.0,10.0,10.0,10.0,10.0,10.0,9.0,19,3.73 +7994,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,0.37 +72030,60.0,8.0,8.0,10.0,8.0,8.0,10.0,1,0.19 +2806,98.0,10.0,9.0,10.0,10.0,9.0,10.0,41,6.8 +38906,97.0,10.0,10.0,10.0,10.0,10.0,8.0,6,1.18 +75556,98.0,10.0,10.0,10.0,10.0,9.0,10.0,9,1.75 +70936,100.0,10.0,9.0,10.0,10.0,10.0,9.0,6,1.64 +74794,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,2.56 +65694,74.0,7.0,6.0,7.0,8.0,8.0,7.0,7,1.17 +12658,85.0,9.0,8.0,10.0,9.0,9.0,9.0,30,4.95 +5706,69.0,7.0,6.0,8.0,8.0,9.0,8.0,11,2.01 +19039,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.42 +54741,,,,,,,,0, +58665,,,,,,,,0, +67416,,,,,,,,0, +12727,,,,,,,,0, +59787,80.0,8.0,10.0,8.0,10.0,10.0,10.0,1,0.46 +16200,97.0,10.0,10.0,10.0,10.0,9.0,10.0,22,3.65 +48817,85.0,9.0,9.0,10.0,10.0,10.0,9.0,22,3.71 +58615,,,,,,,,0, +76506,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.39 +47492,92.0,10.0,10.0,10.0,10.0,10.0,10.0,6,2.34 +55105,90.0,9.0,9.0,10.0,10.0,10.0,9.0,14,2.2 +66576,80.0,8.0,8.0,8.0,8.0,8.0,6.0,1,0.28 +32715,,,,,,,,0, +36862,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.6 +10677,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.29 +65125,,,,,,,,0, +23453,96.0,10.0,10.0,10.0,10.0,9.0,9.0,5,1.26 +7750,98.0,10.0,10.0,10.0,10.0,10.0,10.0,35,5.8 +46806,,,,,,,,0, +35180,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +72310,80.0,9.0,7.0,10.0,9.0,10.0,9.0,3,0.53 +75553,98.0,10.0,10.0,10.0,10.0,9.0,10.0,13,2.03 +65707,98.0,10.0,10.0,10.0,10.0,10.0,9.0,12,2.13 +2707,94.0,9.0,10.0,10.0,10.0,10.0,10.0,11,1.9 +5941,94.0,10.0,10.0,10.0,9.0,10.0,9.0,25,4.12 +52256,90.0,10.0,9.0,9.0,10.0,10.0,8.0,7,1.12 +49140,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.18 +62406,92.0,10.0,9.0,10.0,10.0,10.0,9.0,10,1.6 +19167,,,,,,,,1,0.18 +76531,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.3 +67375,93.0,10.0,9.0,10.0,9.0,10.0,10.0,11,1.88 +52476,99.0,10.0,10.0,10.0,10.0,9.0,10.0,14,2.73 +73735,,,,,,,,1,0.16 +22794,93.0,10.0,10.0,9.0,10.0,9.0,9.0,20,3.43 +39131,97.0,10.0,9.0,10.0,10.0,9.0,10.0,6,1.32 +40776,,,,,,,,0, +15701,,,,,,,,0, +12087,95.0,10.0,10.0,10.0,10.0,10.0,8.0,4,0.69 +72847,93.0,9.0,9.0,9.0,10.0,9.0,9.0,3,0.52 +15093,96.0,10.0,10.0,10.0,10.0,9.0,10.0,22,3.91 +5660,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,1.45 +16191,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.24 +74425,,,,,,,,0, +37157,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.24 +45487,93.0,9.0,8.0,10.0,10.0,8.0,8.0,3,0.51 +39183,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.23 +38753,,,,,,,,0, +10407,94.0,10.0,9.0,10.0,10.0,10.0,9.0,7,1.12 +15651,89.0,9.0,9.0,10.0,10.0,10.0,9.0,13,2.48 +14386,100.0,10.0,10.0,10.0,10.0,9.0,9.0,13,3.28 +15967,82.0,9.0,9.0,9.0,9.0,8.0,8.0,10,1.69 +32023,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.5 +20192,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,1.18 +19109,91.0,10.0,9.0,10.0,9.0,9.0,9.0,15,2.76 +45478,,,,,,,,0, +29880,98.0,10.0,10.0,9.0,10.0,9.0,9.0,11,2.08 +5338,95.0,10.0,9.0,10.0,10.0,10.0,9.0,22,3.79 +63115,,,,,,,,0, +58157,60.0,10.0,4.0,10.0,10.0,10.0,8.0,1,0.2 +9930,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.36 +20072,,,,,,,,1,0.16 +49434,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.25 +67932,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.32 +22855,100.0,10.0,9.0,10.0,10.0,9.0,9.0,3,0.53 +44285,90.0,8.0,9.0,9.0,9.0,9.0,8.0,2,0.36 +11830,,,,,,,,0, +32947,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.22 +55563,93.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.55 +70270,96.0,9.0,9.0,10.0,10.0,9.0,9.0,5,1.22 +15219,,,,,,,,0, +40064,96.0,10.0,10.0,8.0,9.0,9.0,10.0,5,0.86 +32745,100.0,10.0,10.0,10.0,10.0,9.0,10.0,8,1.33 +40509,,,,,,,,0, +63348,,,,,,,,1,0.16 +39944,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,1.69 +56035,,,,,,,,0, +68632,67.0,5.0,6.0,7.0,7.0,8.0,7.0,6,2.0 +11734,91.0,9.0,9.0,10.0,9.0,9.0,9.0,11,1.96 +7383,90.0,9.0,9.0,10.0,10.0,10.0,9.0,31,5.25 +19533,60.0,9.0,6.0,10.0,10.0,9.0,6.0,2,0.5 +57014,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.38 +45391,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.59 +30510,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.14 +58451,,,,,,,,0, +67976,80.0,8.0,6.0,10.0,10.0,10.0,10.0,1,0.34 +70447,100.0,8.0,8.0,10.0,8.0,6.0,8.0,1,0.18 +22312,93.0,9.0,10.0,10.0,10.0,10.0,10.0,6,1.08 +19857,86.0,9.0,9.0,10.0,10.0,10.0,9.0,11,1.74 +54594,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +63278,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.65 +25205,87.0,9.0,10.0,9.0,9.0,10.0,8.0,6,0.99 +29313,97.0,10.0,9.0,10.0,10.0,9.0,10.0,12,2.16 +56612,,,,,,,,0, +66210,90.0,9.0,8.0,8.0,9.0,10.0,9.0,4,0.72 +66690,,,,,,,,0, +72383,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.05 +71176,100.0,9.0,9.0,10.0,10.0,9.0,10.0,2,0.36 +51117,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.38 +69,,,,,,,,0, +37462,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.86 +3529,,,,,,,,0, +16726,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.6 +72986,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.18 +35834,92.0,10.0,10.0,10.0,9.0,10.0,9.0,12,2.17 +60185,,,,,,,,0, +47282,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +6805,56.0,6.0,8.0,7.0,7.0,9.0,6.0,10,1.92 +57977,96.0,10.0,9.0,10.0,10.0,10.0,10.0,5,0.89 +32226,93.0,10.0,10.0,10.0,10.0,9.0,9.0,12,2.07 +11162,76.0,9.0,8.0,8.0,8.0,10.0,8.0,10,1.8 +46947,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.53 +76217,80.0,8.0,9.0,9.0,9.0,8.0,7.0,6,1.29 +54148,,,,,,,,1,0.33 +40349,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.39 +18486,,,,,,,,0, +4202,78.0,8.0,8.0,8.0,8.0,8.0,8.0,10,2.19 +23249,94.0,10.0,10.0,10.0,10.0,9.0,9.0,7,1.3 +76513,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.48 +40011,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.79 +25047,100.0,10.0,10.0,10.0,10.0,10.0,10.0,24,4.11 +71746,89.0,9.0,10.0,10.0,9.0,9.0,9.0,7,1.27 +1790,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,0.9 +28357,93.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.52 +69074,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,0.8 +32295,87.0,10.0,9.0,9.0,10.0,9.0,7.0,3,1.25 +28638,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.37 +15192,92.0,9.0,8.0,10.0,10.0,8.0,9.0,6,0.96 +15588,90.0,9.0,8.0,10.0,10.0,10.0,10.0,3,0.59 +54590,88.0,9.0,9.0,9.0,10.0,8.0,9.0,24,3.98 +22713,97.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.36 +3882,100.0,10.0,8.0,9.0,10.0,10.0,10.0,2,0.58 +69577,96.0,10.0,10.0,10.0,10.0,9.0,10.0,26,4.19 +6542,90.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.77 +29763,80.0,9.0,9.0,10.0,9.0,9.0,9.0,8,1.42 +51208,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,2.31 +48986,98.0,10.0,10.0,10.0,10.0,9.0,10.0,10,1.79 +33431,,,,,,,,0, +17897,100.0,10.0,10.0,9.0,10.0,10.0,10.0,3,0.49 +18930,95.0,10.0,9.0,10.0,10.0,10.0,10.0,12,2.12 +60324,90.0,9.0,9.0,10.0,10.0,10.0,10.0,2,0.48 +29645,90.0,9.0,8.0,10.0,10.0,9.0,9.0,3,0.59 +59673,,,,,,,,0, +40483,,,,,,,,2,0.34 +12733,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,3.75 +17480,60.0,7.0,7.0,7.0,6.0,8.0,6.0,7,1.34 +11626,100.0,10.0,9.0,10.0,9.0,10.0,8.0,2,0.5 +13126,95.0,9.0,10.0,9.0,10.0,10.0,10.0,4,0.82 +50996,73.0,9.0,7.0,8.0,9.0,8.0,8.0,17,3.13 +25191,100.0,10.0,10.0,9.0,9.0,10.0,9.0,2,2.0 +32283,100.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.55 +65669,98.0,10.0,10.0,10.0,10.0,9.0,10.0,16,3.02 +61049,92.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.95 +30942,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.27 +19212,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.21 +68604,88.0,9.0,9.0,10.0,10.0,10.0,9.0,29,4.7 +38944,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.19 +57456,89.0,10.0,8.0,10.0,9.0,10.0,9.0,23,4.26 +5267,,,,,,,,0, +71197,,,,,,,,0, +4018,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,2.28 +73350,95.0,10.0,9.0,10.0,10.0,10.0,10.0,15,2.69 +15715,80.0,8.0,8.0,10.0,10.0,10.0,8.0,1,0.6 +56915,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,0.26 +51878,88.0,10.0,8.0,10.0,10.0,10.0,9.0,19,3.33 +43893,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.16 +26807,,,,,,,,0, +51189,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.18 +71026,96.0,10.0,9.0,10.0,10.0,9.0,9.0,14,2.44 +20144,87.0,9.0,10.0,9.0,10.0,9.0,7.0,3,0.55 +64228,97.0,10.0,9.0,10.0,10.0,10.0,10.0,6,1.15 +27237,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,2.31 +71471,95.0,10.0,10.0,10.0,10.0,10.0,10.0,13,2.57 +76255,92.0,10.0,8.0,10.0,10.0,8.0,9.0,19,3.29 +44720,90.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.48 +55307,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.51 +9296,88.0,9.0,9.0,9.0,9.0,10.0,10.0,22,3.79 +36182,99.0,10.0,10.0,10.0,10.0,10.0,10.0,15,2.63 +14839,93.0,10.0,8.0,10.0,10.0,10.0,9.0,3,2.65 +40744,100.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.55 +1589,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.35 +35505,94.0,10.0,9.0,10.0,10.0,9.0,10.0,10,1.84 +44703,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.18 +17601,100.0,8.0,8.0,8.0,8.0,8.0,10.0,2,0.33 +1679,,,,,,,,0, +6341,,,,,,,,0, +8538,86.0,9.0,9.0,7.0,9.0,8.0,8.0,7,1.21 +18182,100.0,10.0,8.0,10.0,10.0,10.0,10.0,4,0.68 +64191,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.18 +56772,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.19 +15021,92.0,10.0,9.0,10.0,10.0,10.0,9.0,10,1.76 +2754,88.0,10.0,9.0,10.0,10.0,10.0,10.0,12,2.26 +6849,100.0,10.0,10.0,10.0,9.0,10.0,9.0,2,0.71 +68165,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.49 +25318,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,0.95 +60330,47.0,7.0,7.0,9.0,6.0,9.0,7.0,3,0.56 +46659,,,,,,,,0, +56625,70.0,6.0,6.0,9.0,9.0,10.0,7.0,3,0.63 +24905,,,,,,,,0, +16388,,,,,,,,0, +38687,97.0,10.0,10.0,10.0,10.0,10.0,9.0,6,1.03 +20794,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.67 +45482,,,,,,,,0, +105,97.0,10.0,10.0,10.0,10.0,10.0,10.0,12,2.07 +22115,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.78 +44087,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.22 +68152,,,,,,,,0, +37651,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.25 +27608,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,1.25 +55053,,,,,,,,0, +23054,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.85 +48061,100.0,8.0,10.0,8.0,10.0,10.0,10.0,1,0.17 +4410,98.0,10.0,9.0,10.0,10.0,9.0,10.0,10,2.0 +51266,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +35916,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.75 +59273,94.0,10.0,10.0,9.0,10.0,9.0,9.0,7,1.33 +65659,96.0,10.0,10.0,10.0,10.0,10.0,10.0,63,10.56 +50535,100.0,10.0,10.0,10.0,6.0,8.0,10.0,2,0.33 +43279,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.81 +53544,,,,,,,,0, +24863,96.0,9.0,10.0,10.0,10.0,10.0,9.0,16,2.82 +44116,100.0,10.0,10.0,10.0,10.0,10.0,10.0,19,3.41 +38023,92.0,10.0,9.0,9.0,10.0,10.0,9.0,12,3.05 +44832,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.61 +21361,,,,,,,,3,0.51 +992,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.22 +40235,96.0,10.0,9.0,10.0,10.0,9.0,10.0,5,0.9 +46344,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.33 +59506,,,,,,,,0, +43838,80.0,7.0,8.0,9.0,10.0,10.0,7.0,2,0.81 +53358,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.63 +56820,93.0,10.0,8.0,10.0,10.0,10.0,9.0,6,1.18 +15050,99.0,10.0,10.0,10.0,10.0,10.0,10.0,30,5.03 +62494,,,,,,,,1,0.22 +61969,97.0,10.0,10.0,10.0,10.0,9.0,9.0,6,2.4 +49026,95.0,10.0,9.0,10.0,10.0,10.0,10.0,12,2.14 +67164,90.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.37 +29808,94.0,9.0,9.0,8.0,9.0,9.0,10.0,7,1.35 +66496,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.23 +46508,,,,,,,,0, +51313,,,,,,,,0, +66519,100.0,10.0,9.0,10.0,10.0,10.0,9.0,7,1.44 +35004,97.0,10.0,10.0,9.0,10.0,9.0,10.0,15,2.92 +50061,,,,,,,,0, +45898,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.78 +49270,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.19 +33044,,,,,,,,0, +13191,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.9 +34084,,,,,,,,0, +58093,,,,,,,,0, +50270,,,,,,,,0, +52482,,,,,,,,0, +68402,100.0,10.0,9.0,10.0,10.0,6.0,9.0,2,0.36 +67778,,,,,,,,0, +62608,,,,,,,,0, +17126,99.0,10.0,10.0,9.0,10.0,10.0,10.0,17,5.73 +64519,,,,,,,,0, +62970,,,,,,,,0, +52554,93.0,10.0,9.0,10.0,10.0,10.0,9.0,6,1.08 +9171,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,1.96 +19873,,,,,,,,0, +2135,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.36 +33045,,,,,,,,1,0.18 +49164,,,,,,,,0, +27759,,,,,,,,0, +40695,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.84 +3754,,,,,,,,0, +56473,,,,,,,,1,0.17 +6285,,,,,,,,1,0.21 +16813,80.0,10.0,6.0,10.0,10.0,10.0,6.0,1,0.24 +54742,,,,,,,,0, +36961,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.8 +60934,,,,,,,,1,0.18 +58083,86.0,9.0,10.0,8.0,8.0,9.0,8.0,10,1.8 +28983,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.44 +76980,92.0,10.0,10.0,10.0,10.0,10.0,8.0,5,0.93 +65101,84.0,8.0,7.0,9.0,9.0,9.0,9.0,11,3.59 +48863,95.0,9.0,9.0,10.0,10.0,10.0,9.0,13,2.28 +68007,80.0,10.0,8.0,10.0,10.0,10.0,6.0,1,0.81 +14295,98.0,10.0,10.0,10.0,10.0,9.0,10.0,10,1.79 +18787,80.0,10.0,10.0,10.0,10.0,9.0,9.0,5,0.85 +30892,80.0,7.0,7.0,10.0,8.0,10.0,6.0,2,0.43 +30303,98.0,10.0,10.0,10.0,10.0,9.0,10.0,13,2.2 +52459,99.0,10.0,10.0,10.0,10.0,10.0,10.0,17,3.17 +15931,100.0,10.0,10.0,10.0,10.0,8.0,9.0,2,0.7 +26472,,,,,,,,0, +50708,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,2.35 +8285,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.4 +34368,93.0,10.0,10.0,9.0,9.0,10.0,10.0,29,5.21 +34263,,,,,,,,0, +52436,,,,,,,,0, +53069,88.0,9.0,10.0,9.0,9.0,10.0,9.0,8,1.73 +8201,,,,,,,,0, +15309,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.57 +68644,,,,,,,,0, +66400,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.63 +36466,40.0,2.0,10.0,8.0,6.0,2.0,4.0,3,0.51 +60994,80.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.24 +54358,94.0,10.0,10.0,9.0,10.0,9.0,9.0,14,2.47 +61800,100.0,10.0,10.0,10.0,10.0,10.0,10.0,12,4.62 +17843,93.0,10.0,9.0,9.0,9.0,10.0,9.0,43,7.59 +35885,100.0,9.0,9.0,10.0,9.0,9.0,10.0,3,0.69 +23754,100.0,10.0,10.0,10.0,9.0,9.0,8.0,5,1.09 +29510,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.18 +19273,98.0,10.0,10.0,10.0,9.0,9.0,9.0,11,1.9 +1822,,,,,,,,0, +23535,75.0,9.0,7.0,9.0,10.0,8.0,8.0,4,0.86 +31179,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.66 +12484,93.0,10.0,9.0,10.0,10.0,10.0,10.0,10,1.85 +10659,,,,,,,,1,0.17 +69546,,,,,,,,0, +31394,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,1.35 +47528,86.0,8.0,8.0,9.0,9.0,8.0,8.0,10,1.72 +40940,,,,,,,,0, +25009,99.0,10.0,10.0,10.0,10.0,10.0,10.0,16,4.32 +36444,98.0,10.0,10.0,10.0,10.0,9.0,10.0,45,8.08 +46240,,,,,,,,0, +61771,,,,,,,,0, +43194,98.0,10.0,10.0,10.0,10.0,10.0,10.0,11,2.5 +60234,,,,,,,,0, +25478,85.0,9.0,8.0,9.0,10.0,9.0,9.0,17,2.9 +31373,87.0,8.0,10.0,8.0,9.0,9.0,10.0,4,1.45 +7973,,,,,,,,0, +35453,96.0,10.0,9.0,9.0,9.0,9.0,9.0,14,5.92 +72779,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.35 +71155,40.0,10.0,10.0,8.0,2.0,8.0,6.0,1,1.0 +13137,88.0,9.0,9.0,9.0,9.0,10.0,9.0,15,2.59 +19893,54.0,7.0,6.0,9.0,9.0,7.0,6.0,7,1.26 +40700,75.0,7.0,8.0,8.0,9.0,9.0,8.0,4,1.5 +65114,80.0,10.0,10.0,10.0,10.0,9.0,9.0,5,1.25 +3741,98.0,10.0,10.0,10.0,10.0,10.0,9.0,26,4.46 +50155,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.55 +67732,91.0,9.0,9.0,9.0,10.0,10.0,9.0,18,3.18 +7860,,,,,,,,0, +62671,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +50005,90.0,10.0,9.0,10.0,10.0,10.0,9.0,27,4.97 +40187,90.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.68 +33093,99.0,10.0,10.0,10.0,10.0,9.0,10.0,16,2.96 +64242,93.0,9.0,8.0,10.0,9.0,10.0,10.0,8,1.37 +67318,92.0,10.0,10.0,10.0,10.0,8.0,10.0,5,0.93 +23476,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.03 +14796,97.0,10.0,10.0,10.0,10.0,9.0,9.0,13,2.71 +28378,95.0,9.0,10.0,10.0,10.0,8.0,10.0,4,0.75 +35052,100.0,10.0,10.0,10.0,10.0,9.0,10.0,13,2.55 +6349,97.0,9.0,9.0,9.0,10.0,10.0,10.0,6,1.06 +36498,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.23 +32959,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +40486,98.0,10.0,10.0,10.0,10.0,9.0,10.0,9,2.2 +9525,100.0,9.0,10.0,9.0,10.0,9.0,9.0,3,0.89 +68968,89.0,9.0,10.0,9.0,9.0,10.0,8.0,14,3.11 +5604,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +38389,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.08 +55253,94.0,10.0,10.0,10.0,10.0,9.0,10.0,8,1.44 +22348,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.08 +27652,100.0,9.0,10.0,10.0,10.0,10.0,10.0,7,1.35 +26145,93.0,10.0,9.0,9.0,10.0,10.0,9.0,8,1.61 +19288,95.0,10.0,9.0,10.0,10.0,10.0,10.0,15,3.1 +39177,98.0,10.0,10.0,10.0,10.0,10.0,10.0,17,3.31 +21359,94.0,10.0,9.0,10.0,10.0,9.0,9.0,33,5.72 +39992,,,,,,,,0, +37138,90.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.83 +13996,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.43 +60919,95.0,10.0,9.0,10.0,10.0,10.0,10.0,11,2.06 +23762,100.0,9.0,9.0,10.0,10.0,9.0,10.0,2,0.38 +76665,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,3.68 +41833,94.0,10.0,10.0,10.0,10.0,10.0,10.0,11,1.93 +36912,,,,,,,,0, +9064,95.0,10.0,10.0,10.0,10.0,9.0,10.0,16,2.96 +3101,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.53 +54899,90.0,9.0,7.0,9.0,10.0,10.0,8.0,2,0.4 +37838,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.03 +26601,96.0,10.0,8.0,10.0,10.0,10.0,9.0,5,0.92 +38616,,,,,,,,2,0.46 +74598,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +44975,87.0,9.0,10.0,10.0,9.0,10.0,10.0,6,1.35 +36842,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.26 +43837,84.0,9.0,10.0,9.0,9.0,8.0,9.0,9,1.9 +31671,100.0,10.0,10.0,10.0,9.0,10.0,10.0,2,0.71 +73039,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.48 +13413,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.27 +33938,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.92 +65610,90.0,9.0,9.0,10.0,10.0,10.0,9.0,5,1.27 +9570,,,,,,,,0, +5939,96.0,7.0,9.0,10.0,8.0,9.0,10.0,5,0.9 +39008,97.0,10.0,10.0,10.0,9.0,10.0,9.0,19,4.56 +30951,87.0,10.0,8.0,10.0,10.0,9.0,9.0,3,0.56 +2316,,,,,,,,0, +27893,100.0,10.0,9.0,9.0,10.0,10.0,9.0,3,0.65 +64496,90.0,9.0,8.0,10.0,9.0,9.0,8.0,6,1.26 +76000,80.0,5.0,9.0,10.0,9.0,9.0,8.0,3,1.41 +21947,92.0,10.0,9.0,9.0,9.0,9.0,10.0,19,3.58 +9854,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.39 +56530,80.0,9.0,9.0,7.0,8.0,9.0,8.0,11,1.92 +15333,87.0,10.0,10.0,10.0,8.0,9.0,10.0,3,1.41 +46747,,,,,,,,0, +15867,,,,,,,,0, +58993,,,,,,,,0, +41697,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.46 +40203,84.0,10.0,10.0,8.0,9.0,9.0,9.0,5,1.01 +18049,80.0,10.0,8.0,10.0,8.0,8.0,8.0,4,0.72 +38443,80.0,10.0,10.0,8.0,10.0,8.0,8.0,1,0.21 +12809,89.0,10.0,9.0,8.0,10.0,9.0,9.0,9,1.61 +56046,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.78 +40969,90.0,9.0,9.0,9.0,9.0,10.0,9.0,20,3.68 +9452,90.0,9.0,9.0,10.0,10.0,9.0,10.0,3,0.59 +51286,,,,,,,,1,0.24 +638,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.2 +36740,98.0,10.0,10.0,10.0,10.0,10.0,9.0,19,5.59 +38177,,,,,,,,0, +36944,40.0,6.0,2.0,2.0,2.0,6.0,4.0,1,0.18 +50015,,,,,,,,1,1.0 +19898,95.0,10.0,10.0,9.0,9.0,10.0,9.0,4,0.81 +73354,90.0,10.0,8.0,10.0,9.0,9.0,10.0,2,0.39 +59695,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.86 +41613,96.0,10.0,9.0,10.0,10.0,10.0,9.0,6,1.12 +72192,90.0,10.0,8.0,10.0,10.0,10.0,10.0,3,0.72 +6679,94.0,10.0,9.0,9.0,10.0,10.0,10.0,10,2.1 +30218,93.0,9.0,9.0,10.0,10.0,9.0,9.0,3,0.71 +4949,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.51 +11612,,,,,,,,0, +23297,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.19 +39240,85.0,9.0,10.0,10.0,10.0,9.0,10.0,4,0.78 +19159,91.0,10.0,8.0,10.0,10.0,10.0,9.0,9,2.01 +68845,88.0,10.0,7.0,10.0,10.0,9.0,10.0,5,0.93 +68119,89.0,10.0,9.0,10.0,10.0,9.0,9.0,7,1.44 +997,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,1.0 +61428,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,1.06 +6477,97.0,10.0,10.0,9.0,10.0,10.0,9.0,15,2.9 +59426,,,,,,,,0, +76265,91.0,10.0,9.0,9.0,10.0,10.0,9.0,11,2.19 +11596,90.0,10.0,9.0,10.0,10.0,10.0,9.0,12,2.9 +36803,90.0,10.0,10.0,9.0,10.0,9.0,9.0,8,1.62 +35848,97.0,10.0,10.0,10.0,10.0,9.0,10.0,6,1.38 +17141,,,,,,,,0, +31034,94.0,10.0,10.0,10.0,10.0,10.0,9.0,20,4.29 +53433,,,,,,,,0, +67715,,,,,,,,0, +27753,87.0,9.0,9.0,10.0,9.0,10.0,9.0,26,5.2 +22505,,,,,,,,0, +108,,,,,,,,0, +48929,92.0,10.0,9.0,9.0,10.0,9.0,9.0,10,1.96 +34544,90.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.67 +57064,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.34 +26108,93.0,9.0,9.0,10.0,9.0,9.0,9.0,6,1.22 +19895,87.0,9.0,9.0,8.0,9.0,9.0,9.0,14,2.88 +46851,80.0,9.0,8.0,10.0,10.0,10.0,9.0,2,0.38 +29788,,,,,,,,0, +42872,,,,,,,,0, +57431,,,,,,,,0, +46621,80.0,8.0,10.0,10.0,10.0,8.0,8.0,1,0.33 +45456,89.0,9.0,9.0,9.0,9.0,9.0,9.0,11,2.23 +16048,,,,,,,,0, +62319,,,,,,,,0, +11721,100.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.21 +48368,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.19 +71153,60.0,4.0,10.0,6.0,8.0,10.0,10.0,1,0.21 +2244,80.0,10.0,6.0,10.0,10.0,10.0,8.0,1,0.26 +13918,90.0,9.0,6.0,10.0,9.0,9.0,10.0,2,0.43 +43299,80.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.37 +9502,100.0,8.0,8.0,10.0,10.0,10.0,8.0,1,0.22 +71006,,,,,,,,1,1.0 +8378,98.0,10.0,10.0,10.0,10.0,10.0,10.0,19,3.68 +67784,100.0,10.0,10.0,10.0,10.0,10.0,9.0,22,4.55 +10962,99.0,10.0,10.0,10.0,10.0,9.0,10.0,21,4.17 +64365,94.0,10.0,10.0,9.0,10.0,9.0,10.0,7,2.14 +38255,,,,,,,,0, +63054,87.0,9.0,9.0,10.0,10.0,9.0,9.0,15,3.1 +74614,,,,,,,,0, +15101,97.0,10.0,10.0,10.0,10.0,9.0,10.0,31,5.81 +43717,,,,,,,,0, +10311,80.0,10.0,4.0,10.0,10.0,10.0,10.0,2,0.44 +41072,95.0,10.0,10.0,10.0,10.0,10.0,9.0,12,2.35 +74843,,,,,,,,0, +58259,99.0,10.0,10.0,10.0,10.0,9.0,9.0,33,6.11 +10172,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.86 +62502,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.23 +7928,,,,,,,,1,0.19 +70252,100.0,10.0,10.0,10.0,10.0,9.0,10.0,8,1.45 +50910,94.0,10.0,10.0,10.0,10.0,10.0,9.0,7,1.51 +33077,100.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.39 +31718,93.0,9.0,9.0,9.0,10.0,10.0,9.0,6,1.33 +33444,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.38 +24277,,,,,,,,0, +46664,95.0,10.0,10.0,10.0,10.0,9.0,9.0,11,2.08 +13380,92.0,9.0,9.0,9.0,10.0,10.0,9.0,5,0.98 +25247,,,,,,,,0, +16332,91.0,10.0,10.0,10.0,10.0,10.0,9.0,13,2.87 +16320,92.0,9.0,8.0,9.0,9.0,10.0,9.0,19,3.52 +10571,97.0,9.0,9.0,10.0,10.0,10.0,9.0,7,1.3 +24085,,,,,,,,0, +60827,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.29 +44176,86.0,9.0,9.0,8.0,10.0,10.0,9.0,10,2.24 +42037,93.0,9.0,9.0,10.0,10.0,10.0,9.0,17,3.29 +40834,,,,,,,,0, +3271,90.0,10.0,10.0,10.0,10.0,10.0,9.0,39,7.6 +55894,93.0,10.0,10.0,10.0,10.0,10.0,10.0,20,4.0 +42192,92.0,9.0,9.0,10.0,10.0,10.0,9.0,13,2.57 +22338,,,,,,,,0, +21320,98.0,9.0,10.0,9.0,10.0,10.0,10.0,10,1.96 +31849,,,,,,,,0, +65539,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.43 +60575,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,3.73 +40009,93.0,10.0,9.0,10.0,10.0,9.0,8.0,3,0.87 +75958,93.0,10.0,9.0,9.0,9.0,10.0,10.0,3,0.87 +25382,100.0,9.0,9.0,10.0,10.0,10.0,10.0,4,0.83 +65243,99.0,10.0,9.0,10.0,10.0,10.0,10.0,14,2.75 +53981,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.23 +15891,100.0,10.0,10.0,10.0,9.0,10.0,10.0,5,1.08 +14981,100.0,9.0,10.0,10.0,10.0,10.0,10.0,4,0.81 +59372,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.56 +48822,,,,,,,,0, +70788,95.0,10.0,10.0,10.0,10.0,10.0,10.0,18,3.75 +1866,87.0,9.0,9.0,9.0,10.0,9.0,9.0,20,3.92 +55002,90.0,9.0,7.0,9.0,10.0,10.0,9.0,6,1.23 +16058,98.0,10.0,10.0,10.0,10.0,10.0,10.0,25,4.84 +6891,89.0,9.0,10.0,9.0,9.0,10.0,9.0,9,2.14 +52474,92.0,10.0,8.0,9.0,10.0,8.0,9.0,11,2.31 +4884,,,,,,,,0, +75203,94.0,10.0,10.0,10.0,10.0,9.0,9.0,13,2.58 +70533,91.0,10.0,9.0,10.0,10.0,10.0,9.0,15,3.0 +68300,,,,,,,,0, +341,99.0,10.0,10.0,10.0,10.0,10.0,10.0,31,5.96 +6499,95.0,10.0,10.0,9.0,10.0,10.0,9.0,13,3.94 +34073,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.21 +59271,87.0,9.0,7.0,10.0,10.0,9.0,10.0,7,1.28 +60867,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.2 +12251,100.0,9.0,10.0,10.0,10.0,10.0,10.0,4,0.78 +1968,60.0,8.0,7.0,7.0,7.0,7.0,5.0,5,1.47 +6055,,,,,,,,0, +66141,,,,,,,,0, +22872,94.0,9.0,9.0,10.0,10.0,10.0,9.0,16,3.36 +60544,,,,,,,,0, +48613,60.0,6.0,6.0,10.0,10.0,6.0,2.0,1,0.48 +18803,80.0,10.0,6.0,10.0,10.0,10.0,8.0,1,0.4 +2970,100.0,10.0,10.0,10.0,6.0,10.0,10.0,1,1.0 +52386,96.0,10.0,10.0,9.0,10.0,10.0,10.0,14,2.75 +32239,96.0,10.0,9.0,10.0,10.0,10.0,10.0,33,6.51 +61478,100.0,10.0,10.0,8.0,10.0,10.0,8.0,1,0.75 +24417,85.0,9.0,8.0,9.0,9.0,9.0,9.0,11,2.84 +40048,86.0,9.0,9.0,10.0,10.0,9.0,8.0,14,2.73 +43862,,,,,,,,0, +1730,,,,,,,,0, +12785,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.39 +45825,80.0,9.0,10.0,9.0,9.0,10.0,9.0,2,0.55 +42867,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.41 +71355,,,,,,,,0, +37628,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.36 +70352,,,,,,,,0, +67681,,,,,,,,0, +56623,,,,,,,,0, +11692,,,,,,,,0, +64794,,,,,,,,0, +32519,,,,,,,,0, +64725,87.0,9.0,9.0,10.0,10.0,10.0,9.0,9,4.66 +31196,80.0,8.0,7.0,9.0,9.0,9.0,9.0,3,0.83 +40813,91.0,9.0,9.0,9.0,9.0,9.0,8.0,7,1.49 +56277,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.89 +67675,90.0,10.0,9.0,10.0,10.0,9.0,10.0,2,0.47 +54975,100.0,10.0,10.0,10.0,9.0,9.0,10.0,3,0.6 +24595,88.0,10.0,8.0,10.0,10.0,8.0,10.0,5,0.99 +12614,91.0,9.0,10.0,9.0,9.0,10.0,9.0,11,2.26 +25144,78.0,9.0,9.0,9.0,9.0,9.0,8.0,9,1.89 +54707,100.0,9.0,9.0,9.0,9.0,9.0,9.0,2,1.87 +9326,100.0,10.0,9.0,10.0,10.0,10.0,10.0,6,1.17 +73146,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.36 +67293,,,,,,,,1,0.21 +6212,97.0,10.0,10.0,10.0,10.0,10.0,10.0,64,11.93 +50454,96.0,10.0,10.0,10.0,10.0,10.0,9.0,52,9.81 +990,,,,,,,,0, +54040,80.0,9.0,8.0,10.0,10.0,9.0,9.0,7,1.29 +67050,80.0,8.0,4.0,8.0,8.0,10.0,6.0,1,0.22 +64108,90.0,9.0,9.0,10.0,10.0,9.0,9.0,10,4.17 +17957,88.0,10.0,9.0,10.0,10.0,10.0,9.0,8,1.64 +36659,90.0,9.0,8.0,10.0,10.0,9.0,9.0,5,1.29 +61423,,,,,,,,0, +40986,91.0,9.0,10.0,9.0,10.0,10.0,9.0,7,1.35 +64554,87.0,9.0,7.0,9.0,10.0,9.0,9.0,4,0.74 +63389,60.0,8.0,8.0,6.0,6.0,8.0,8.0,2,0.44 +4726,80.0,8.0,7.0,9.0,9.0,9.0,8.0,7,1.32 +63264,97.0,10.0,10.0,10.0,9.0,10.0,10.0,6,1.21 +70361,84.0,9.0,8.0,10.0,9.0,9.0,9.0,9,2.65 +7599,85.0,9.0,9.0,9.0,8.0,9.0,9.0,8,2.18 +18085,92.0,9.0,9.0,10.0,10.0,10.0,9.0,5,1.03 +54831,100.0,10.0,10.0,10.0,10.0,10.0,10.0,49,9.3 +48708,97.0,10.0,9.0,10.0,10.0,10.0,10.0,7,1.35 +53101,94.0,10.0,9.0,10.0,10.0,9.0,10.0,17,3.38 +46634,,,,,,,,0, +56455,,,,,,,,0, +61728,,,,,,,,0, +42343,,,,,,,,0, +71372,98.0,10.0,10.0,10.0,10.0,10.0,9.0,13,3.12 +56389,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,0.83 +30533,90.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.83 +62782,80.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.19 +21295,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.41 +33075,87.0,10.0,10.0,9.0,9.0,9.0,9.0,7,1.58 +76176,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.19 +67970,,,,,,,,0, +15180,93.0,9.0,9.0,8.0,10.0,10.0,9.0,6,1.28 +64857,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.39 +8022,80.0,9.0,8.0,10.0,8.0,10.0,8.0,7,1.38 +69736,,,,,,,,1,0.21 +36462,96.0,10.0,10.0,10.0,10.0,10.0,10.0,16,3.81 +47281,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.21 +40138,95.0,10.0,10.0,10.0,10.0,10.0,9.0,38,7.76 +68240,,,,,,,,0, +38993,98.0,10.0,9.0,10.0,10.0,10.0,10.0,19,3.61 +214,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.79 +66075,90.0,10.0,10.0,10.0,10.0,8.0,10.0,4,1.74 +76474,,,,,,,,0, +58036,,,,,,,,0, +34759,,,,,,,,0, +29549,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.36 +55120,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,3.67 +32975,88.0,10.0,9.0,9.0,10.0,10.0,10.0,5,1.0 +68325,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.38 +76421,,,,,,,,0, +48636,87.0,9.0,9.0,10.0,10.0,9.0,10.0,4,0.78 +31494,97.0,10.0,10.0,9.0,10.0,10.0,10.0,12,3.05 +47415,93.0,10.0,10.0,10.0,9.0,9.0,9.0,4,1.17 +72424,97.0,10.0,10.0,10.0,10.0,9.0,9.0,6,1.18 +32972,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.22 +60200,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.19 +41797,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.32 +64521,,,,,,,,0, +58184,,,,,,,,0, +76224,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.27 +33460,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.29 +37866,,,,,,,,0, +19329,96.0,10.0,9.0,10.0,10.0,10.0,9.0,14,2.84 +28360,,,,,,,,0, +74634,100.0,10.0,9.0,10.0,10.0,10.0,10.0,9,2.84 +23733,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.65 +19129,92.0,10.0,9.0,10.0,10.0,10.0,9.0,5,1.28 +4854,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.27 +14468,89.0,9.0,8.0,10.0,10.0,9.0,8.0,9,2.11 +54348,94.0,9.0,9.0,10.0,9.0,10.0,9.0,11,4.85 +74917,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +42527,73.0,7.0,7.0,7.0,7.0,9.0,7.0,4,1.01 +14950,100.0,9.0,10.0,10.0,10.0,10.0,9.0,2,0.94 +9610,90.0,9.0,9.0,10.0,10.0,10.0,9.0,2,0.45 +21679,85.0,10.0,8.0,10.0,10.0,9.0,10.0,6,2.07 +55123,95.0,9.0,9.0,10.0,10.0,10.0,9.0,12,2.47 +44493,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.34 +64541,99.0,10.0,10.0,10.0,10.0,10.0,10.0,25,4.9 +2805,100.0,10.0,10.0,10.0,10.0,9.0,10.0,8,1.57 +12117,80.0,8.0,8.0,8.0,10.0,8.0,8.0,1,0.49 +17095,80.0,8.0,9.0,8.0,9.0,9.0,8.0,5,1.12 +45841,,,,,,,,0, +37058,,,,,,,,1, +9957,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.26 +30887,80.0,7.0,7.0,7.0,8.0,9.0,8.0,2,0.41 +34388,,,,,,,,0, +55639,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.79 +38196,60.0,8.0,6.0,10.0,8.0,10.0,10.0,1,0.24 +65950,,,,,,,,0, +27564,98.0,10.0,10.0,10.0,10.0,10.0,10.0,16,3.29 +28184,87.0,9.0,9.0,9.0,9.0,9.0,9.0,4,0.77 +12416,,,,,,,,0, +65115,95.0,10.0,9.0,10.0,10.0,9.0,10.0,4,0.94 +11562,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.39 +73732,72.0,9.0,7.0,10.0,9.0,9.0,7.0,5,1.7 +24865,90.0,10.0,9.0,10.0,10.0,9.0,10.0,2,0.38 +55399,60.0,4.0,4.0,10.0,10.0,10.0,10.0,1,1.0 +75396,80.0,10.0,8.0,10.0,10.0,10.0,10.0,1,1.0 +72593,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.19 +11943,,,,,,,,0, +49586,,,,,,,,0, +16442,80.0,9.0,9.0,9.0,9.0,10.0,8.0,12,3.96 +734,88.0,9.0,8.0,9.0,9.0,10.0,8.0,5,1.03 +44118,,,,,,,,1,0.26 +50308,,,,,,,,0, +8445,,,,,,,,0, +10482,96.0,10.0,9.0,8.0,9.0,10.0,9.0,9,1.86 +59406,91.0,9.0,9.0,9.0,10.0,10.0,9.0,8,1.71 +64909,92.0,9.0,9.0,9.0,9.0,8.0,9.0,5,1.14 +75993,85.0,10.0,9.0,9.0,9.0,10.0,9.0,4,0.82 +29890,85.0,10.0,8.0,9.0,9.0,10.0,9.0,11,2.26 +75124,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.41 +38903,80.0,9.0,10.0,10.0,10.0,10.0,7.0,2,0.68 +9328,99.0,10.0,10.0,10.0,10.0,10.0,10.0,19,3.73 +14740,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.97 +30257,96.0,10.0,9.0,10.0,10.0,10.0,9.0,9,1.76 +1420,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,1.37 +34255,75.0,7.0,9.0,7.0,7.0,7.0,7.0,4,0.86 +65568,,,,,,,,0, +64107,,,,,,,,0, +69285,90.0,9.0,10.0,9.0,10.0,10.0,9.0,7,1.98 +17902,80.0,9.0,8.0,10.0,10.0,9.0,8.0,5,1.04 +57745,89.0,10.0,10.0,10.0,9.0,9.0,10.0,7,1.36 +7122,,,,,,,,0, +40054,,,,,,,,0, +29138,,,,,,,,0, +67076,,,,,,,,0, +24959,,,,,,,,0, +15064,88.0,10.0,9.0,9.0,10.0,10.0,9.0,5,1.01 +15899,90.0,8.0,7.0,7.0,10.0,8.0,8.0,3,0.58 +56794,93.0,9.0,10.0,9.0,10.0,10.0,9.0,6,2.54 +5059,89.0,9.0,9.0,10.0,9.0,9.0,9.0,31,6.0 +54782,84.0,9.0,10.0,8.0,7.0,10.0,9.0,5,0.97 +60303,100.0,10.0,9.0,9.0,10.0,10.0,10.0,2,0.9 +26870,,,,,,,,0, +43597,,,,,,,,0, +62358,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.78 +1818,83.0,8.0,8.0,8.0,9.0,9.0,9.0,7,1.42 +11614,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.21 +47948,80.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.32 +63741,80.0,8.0,8.0,10.0,10.0,10.0,10.0,1,0.28 +54488,99.0,10.0,10.0,10.0,10.0,9.0,10.0,28,5.96 +24171,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.53 +32645,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.4 +26780,98.0,10.0,10.0,10.0,10.0,9.0,9.0,11,2.37 +29168,100.0,10.0,9.0,10.0,9.0,9.0,9.0,4,1.3 +25041,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.59 +40964,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.26 +45430,77.0,8.0,8.0,9.0,7.0,10.0,7.0,7,1.78 +64986,80.0,9.0,6.0,9.0,9.0,7.0,8.0,4,0.79 +37777,,,,,,,,0, +72075,80.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.81 +33403,,,,,,,,0, +74569,93.0,10.0,9.0,10.0,10.0,10.0,9.0,17,3.54 +12927,100.0,10.0,8.0,10.0,8.0,10.0,10.0,1,0.27 +71222,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,1.43 +4669,,,,,,,,0, +51885,,,,,,,,1,0.2 +13635,90.0,10.0,7.0,10.0,10.0,10.0,9.0,4,0.83 +10687,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,4.08 +59156,94.0,10.0,10.0,10.0,10.0,10.0,9.0,7,1.43 +18190,90.0,9.0,9.0,10.0,9.0,10.0,9.0,4,2.22 +19481,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,2.89 +34346,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.43 +31403,,,,,,,,0, +4989,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.41 +29786,,,,,,,,0, +61934,,,,,,,,0, +1443,,,,,,,,0, +46341,,,,,,,,0, +10896,,,,,,,,0, +2753,100.0,10.0,10.0,10.0,10.0,10.0,9.0,8,1.85 +18370,93.0,10.0,9.0,10.0,10.0,8.0,9.0,14,4.38 +41398,93.0,10.0,9.0,10.0,10.0,10.0,9.0,3,0.9 +25988,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.21 +38551,93.0,10.0,10.0,10.0,9.0,9.0,10.0,3,2.2 +19939,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.32 +18638,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.78 +18678,94.0,10.0,10.0,10.0,10.0,10.0,9.0,20,4.08 +69451,83.0,9.0,8.0,9.0,10.0,9.0,8.0,12,2.73 +58819,,,,,,,,2,0.45 +71607,94.0,10.0,10.0,10.0,10.0,10.0,10.0,25,5.24 +52969,,,,,,,,0, +15657,,,,,,,,0, +49064,,,,,,,,0, +35428,,,,,,,,0, +59871,60.0,10.0,6.0,10.0,6.0,8.0,5.0,2,0.75 +44947,99.0,10.0,10.0,10.0,10.0,10.0,10.0,25,6.1 +226,,,,,,,,0, +38934,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.73 +76126,91.0,10.0,10.0,10.0,10.0,10.0,9.0,16,5.65 +8793,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.94 +51023,95.0,10.0,10.0,10.0,10.0,9.0,9.0,4,0.86 +39904,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.74 +40434,90.0,9.0,9.0,9.0,9.0,9.0,9.0,12,2.45 +39791,,,,,,,,0, +12966,87.0,9.0,8.0,10.0,10.0,10.0,8.0,12,2.42 +17178,95.0,10.0,10.0,10.0,10.0,9.0,10.0,12,2.61 +44915,90.0,10.0,10.0,8.0,10.0,9.0,9.0,3,0.93 +3670,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.24 +69768,,,,,,,,0, +38253,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.02 +59155,88.0,9.0,9.0,10.0,10.0,10.0,9.0,12,2.67 +1736,95.0,10.0,10.0,10.0,10.0,10.0,9.0,22,4.85 +50553,100.0,10.0,10.0,10.0,10.0,9.0,10.0,9,2.09 +42083,80.0,9.0,8.0,9.0,9.0,8.0,9.0,7,1.57 +64730,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.52 +61445,,,,,,,,1,0.22 +30745,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.8 +39516,100.0,10.0,9.0,9.0,9.0,10.0,10.0,3,0.92 +60810,75.0,8.0,7.0,9.0,9.0,9.0,7.0,4,1.19 +58373,60.0,5.0,4.0,7.0,9.0,9.0,5.0,2,0.51 +58810,100.0,10.0,10.0,10.0,10.0,8.0,10.0,5,1.15 +62072,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.64 +705,97.0,10.0,10.0,9.0,10.0,10.0,9.0,18,5.35 +74710,,,,,,,,0, +74237,97.0,10.0,10.0,10.0,10.0,10.0,10.0,21,4.7 +56326,,,,,,,,0, +75804,,,,,,,,1,0.67 +31301,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,2.6 +64488,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.29 +19280,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.25 +49464,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.48 +75770,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,1.15 +5331,96.0,10.0,9.0,10.0,10.0,9.0,9.0,10,2.04 +13674,88.0,8.0,8.0,9.0,9.0,9.0,9.0,5,1.24 +39299,93.0,9.0,9.0,9.0,10.0,9.0,10.0,6,2.43 +36317,,,,,,,,0, +62742,100.0,10.0,10.0,10.0,10.0,9.0,9.0,15,3.08 +45585,91.0,10.0,10.0,10.0,10.0,9.0,10.0,14,2.94 +43405,82.0,9.0,9.0,9.0,9.0,8.0,8.0,9,2.01 +1786,,,,,,,,0, +67854,,,,,,,,0, +6720,97.0,10.0,10.0,9.0,10.0,10.0,9.0,12,2.88 +27578,,,,,,,,0, +39848,87.0,9.0,7.0,9.0,9.0,8.0,9.0,3,0.64 +64046,93.0,9.0,8.0,9.0,10.0,9.0,10.0,3,0.79 +35152,85.0,9.0,9.0,10.0,10.0,10.0,9.0,11,2.29 +73950,87.0,9.0,10.0,9.0,8.0,8.0,9.0,9,2.73 +58720,90.0,9.0,7.0,10.0,10.0,9.0,9.0,2,0.77 +10782,85.0,9.0,9.0,9.0,10.0,9.0,8.0,15,4.21 +65891,86.0,9.0,9.0,10.0,10.0,10.0,9.0,10,2.42 +72947,76.0,9.0,9.0,9.0,9.0,10.0,8.0,10,2.26 +25268,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.75 +34801,80.0,9.0,10.0,10.0,10.0,9.0,9.0,3,0.67 +73257,,,,,,,,0, +68983,83.0,9.0,7.0,10.0,9.0,10.0,9.0,13,3.02 +67988,,,,,,,,0, +30045,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.63 +13305,,,,,,,,0, +75734,80.0,9.0,6.0,10.0,10.0,10.0,7.0,3,1.0 +67877,98.0,10.0,10.0,9.0,10.0,10.0,10.0,12,4.0 +53533,80.0,9.0,6.0,10.0,10.0,9.0,8.0,3,0.72 +43844,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,0.61 +35997,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,2.56 +3258,,,,,,,,0, +62840,92.0,10.0,9.0,10.0,10.0,10.0,10.0,5,1.21 +37677,,,,,,,,0, +76493,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.45 +41561,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,1.51 +34372,91.0,9.0,10.0,9.0,10.0,10.0,9.0,11,2.5 +73774,80.0,8.0,9.0,8.0,9.0,9.0,7.0,4,0.98 +16295,83.0,9.0,8.0,10.0,9.0,10.0,8.0,13,3.05 +41231,,,,,,,,0, +72491,,,,,,,,0, +62756,96.0,10.0,9.0,10.0,10.0,10.0,10.0,19,3.93 +9390,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,0.82 +55278,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.24 +18743,,,,,,,,0, +8176,,,,,,,,0, +8707,99.0,10.0,10.0,10.0,10.0,10.0,10.0,16,3.58 +27305,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,1.96 +32697,100.0,10.0,10.0,10.0,10.0,10.0,9.0,7,2.0 +44434,80.0,8.0,9.0,10.0,10.0,9.0,9.0,2,0.86 +8013,80.0,10.0,6.0,10.0,10.0,8.0,8.0,1,0.29 +37999,80.0,9.0,7.0,9.0,10.0,9.0,9.0,2,2.0 +42039,100.0,9.0,10.0,10.0,10.0,8.0,9.0,4,0.83 +56841,75.0,8.0,8.0,10.0,10.0,9.0,8.0,16,4.62 +73557,92.0,10.0,10.0,10.0,10.0,10.0,9.0,36,7.88 +12245,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.29 +10926,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,2.36 +20947,,,,,,,,0, +73782,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,2.37 +58467,,,,,,,,0, +56956,98.0,10.0,10.0,10.0,10.0,9.0,10.0,19,4.35 +59537,93.0,10.0,9.0,10.0,10.0,10.0,9.0,8,2.42 +59052,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.23 +2126,90.0,10.0,10.0,10.0,10.0,9.0,9.0,6,1.34 +38465,,,,,,,,0, +49982,67.0,9.0,9.0,10.0,9.0,10.0,8.0,3,0.7 +53649,,,,,,,,0, +44203,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.45 +17206,96.0,10.0,10.0,10.0,10.0,10.0,9.0,11,2.58 +35451,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.97 +55141,87.0,9.0,9.0,9.0,8.0,10.0,9.0,12,2.73 +33813,99.0,10.0,10.0,10.0,10.0,9.0,10.0,15,3.31 +15502,90.0,9.0,10.0,10.0,10.0,10.0,8.0,4,0.98 +11472,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.79 +73590,73.0,8.0,9.0,9.0,8.0,10.0,8.0,3,0.76 +62205,,,,,,,,0, +3117,,,,,,,,0, +39100,100.0,8.0,10.0,10.0,10.0,10.0,10.0,2,0.52 +34269,97.0,10.0,9.0,10.0,10.0,9.0,9.0,6,1.62 +44124,,,,,,,,0, +48312,91.0,10.0,10.0,9.0,9.0,10.0,9.0,7,2.08 +1138,96.0,10.0,10.0,10.0,9.0,9.0,10.0,14,5.75 +42596,98.0,10.0,9.0,10.0,10.0,10.0,10.0,11,2.89 +24493,93.0,10.0,9.0,10.0,10.0,9.0,10.0,14,3.31 +12429,100.0,10.0,10.0,10.0,9.0,10.0,10.0,7,1.6 +59337,86.0,9.0,8.0,9.0,9.0,9.0,9.0,14,3.5 +19658,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.27 +24981,92.0,10.0,10.0,10.0,9.0,10.0,9.0,28,6.77 +74828,86.0,9.0,10.0,10.0,10.0,10.0,8.0,35,8.27 +5700,96.0,10.0,10.0,10.0,10.0,10.0,9.0,20,4.96 +66982,100.0,10.0,9.0,9.0,10.0,10.0,10.0,2,0.48 +48149,80.0,9.0,8.0,10.0,10.0,10.0,8.0,4,1.54 +4487,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.45 +62414,,,,,,,,0, +56801,91.0,9.0,9.0,10.0,10.0,10.0,9.0,13,2.91 +48585,90.0,10.0,9.0,10.0,10.0,9.0,10.0,2,1.46 +67134,80.0,10.0,10.0,8.0,10.0,10.0,10.0,2,0.71 +65699,91.0,10.0,9.0,10.0,10.0,10.0,10.0,9,3.33 +73998,95.0,10.0,10.0,9.0,9.0,10.0,9.0,5,1.28 +48305,,,,,,,,0, +38071,,,,,,,,0, +15656,,,,,,,,0, +29592,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.43 +57043,,,,,,,,0, +7955,,,,,,,,0, +29469,,,,,,,,0, +18375,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.85 +76052,,,,,,,,0, +25852,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.26 +19093,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.53 +18922,97.0,10.0,10.0,10.0,10.0,9.0,10.0,6,2.43 +34407,88.0,9.0,10.0,10.0,10.0,9.0,9.0,13,4.33 +22046,,,,,,,,0, +39596,98.0,10.0,10.0,10.0,10.0,9.0,10.0,21,7.08 +37260,,,,,,,,0, +35583,100.0,10.0,9.0,10.0,10.0,8.0,10.0,2,0.62 +17986,90.0,9.0,9.0,10.0,10.0,9.0,10.0,8,2.14 +69098,,,,,,,,0, +21089,70.0,8.0,9.0,7.0,8.0,10.0,9.0,2,0.66 +43538,87.0,10.0,9.0,10.0,10.0,10.0,9.0,4,0.98 +59801,80.0,9.0,7.0,9.0,9.0,10.0,8.0,5,2.21 +50090,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.34 +25526,98.0,10.0,10.0,10.0,10.0,10.0,10.0,13,2.95 +67198,,,,,,,,0, +66138,,,,,,,,0, +54207,87.0,9.0,10.0,9.0,9.0,9.0,9.0,14,3.11 +55981,,,,,,,,1,0.28 +53330,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.34 +26733,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.91 +37134,91.0,10.0,9.0,10.0,8.0,10.0,9.0,9,2.43 +48778,89.0,9.0,10.0,9.0,9.0,10.0,9.0,7,2.02 +24840,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.74 +20078,98.0,10.0,9.0,10.0,10.0,9.0,10.0,12,2.69 +14052,84.0,9.0,9.0,9.0,10.0,10.0,8.0,10,2.34 +47101,92.0,9.0,9.0,10.0,9.0,10.0,9.0,13,3.07 +42999,,,,,,,,0, +23991,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,1.96 +57249,,,,,,,,0, +17907,81.0,8.0,8.0,8.0,9.0,10.0,9.0,24,5.22 +8301,91.0,10.0,8.0,9.0,9.0,9.0,10.0,8,2.67 +51330,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.76 +70097,95.0,10.0,10.0,10.0,10.0,10.0,10.0,13,2.93 +904,100.0,10.0,9.0,10.0,10.0,9.0,10.0,2,0.49 +4572,,,,,,,,0, +53584,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.67 +18686,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.44 +34405,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.44 +45872,80.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.43 +29608,96.0,10.0,10.0,10.0,10.0,10.0,8.0,5,1.14 +43329,75.0,8.0,7.0,8.0,9.0,10.0,8.0,24,5.5 +67868,85.0,9.0,8.0,10.0,10.0,10.0,9.0,21,4.63 +14956,73.0,8.0,7.0,8.0,9.0,10.0,8.0,24,5.14 +41519,100.0,10.0,6.0,10.0,10.0,10.0,10.0,2,0.8 +21164,96.0,9.0,9.0,9.0,9.0,10.0,9.0,21,5.29 +13706,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.5 +37100,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.72 +58406,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +52594,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,0.31 +13506,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,3.14 +61806,,,,,,,,0, +4521,95.0,9.0,9.0,10.0,10.0,10.0,10.0,17,4.08 +13616,98.0,10.0,10.0,10.0,10.0,10.0,10.0,11,2.68 +58958,98.0,10.0,10.0,10.0,10.0,10.0,10.0,33,7.67 +5253,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,2.57 +38517,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.33 +10707,,,,,,,,0, +62740,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.04 +74656,100.0,8.0,8.0,10.0,10.0,8.0,10.0,1,1.0 +4511,90.0,9.0,9.0,10.0,9.0,9.0,9.0,21,4.74 +11231,,,,,,,,1,0.58 +68189,81.0,9.0,8.0,10.0,10.0,9.0,9.0,17,3.78 +53725,82.0,9.0,8.0,10.0,9.0,9.0,8.0,30,6.67 +4396,88.0,9.0,9.0,9.0,9.0,10.0,9.0,24,5.29 +40879,86.0,9.0,8.0,10.0,9.0,9.0,9.0,27,6.28 +61836,84.0,9.0,9.0,9.0,9.0,9.0,9.0,18,4.54 +47541,88.0,10.0,8.0,10.0,10.0,10.0,9.0,17,4.32 +44205,88.0,10.0,9.0,10.0,10.0,10.0,9.0,23,5.23 +34901,,,,,,,,0, +53489,100.0,10.0,10.0,10.0,10.0,10.0,9.0,5,2.38 +20150,76.0,9.0,8.0,10.0,9.0,9.0,8.0,16,3.97 +11210,80.0,8.0,6.0,10.0,8.0,8.0,10.0,1,0.63 +60125,75.0,7.0,7.0,9.0,10.0,9.0,9.0,4,1.26 +34194,98.0,10.0,10.0,10.0,10.0,9.0,10.0,11,2.6 +22354,95.0,10.0,10.0,10.0,10.0,10.0,10.0,8,2.42 +28282,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,3.72 +42929,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,1.54 +53914,73.0,7.0,7.0,10.0,10.0,10.0,7.0,3,0.89 +17130,80.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.29 +21715,100.0,10.0,9.0,10.0,10.0,10.0,10.0,4,1.07 +47425,,,,,,,,0, +9856,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.57 +76932,95.0,10.0,8.0,10.0,9.0,10.0,10.0,4,0.91 +75943,85.0,9.0,9.0,9.0,9.0,9.0,8.0,12,2.98 +29785,80.0,8.0,10.0,10.0,8.0,8.0,8.0,1,0.88 +10559,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.54 +10983,,,,,,,,0, +4301,67.0,7.0,7.0,8.0,9.0,9.0,7.0,3,0.88 +50589,90.0,9.0,10.0,9.0,9.0,8.0,9.0,5,1.1 +40915,100.0,10.0,10.0,10.0,10.0,10.0,9.0,9,3.86 +48756,100.0,10.0,10.0,10.0,9.0,9.0,10.0,11,2.66 +58795,88.0,9.0,9.0,7.0,8.0,10.0,9.0,10,2.46 +25157,87.0,9.0,9.0,9.0,9.0,9.0,9.0,11,2.64 +56312,87.0,9.0,10.0,10.0,10.0,8.0,9.0,3,0.88 +49235,,,,,,,,0, +6969,,,,,,,,0, +23534,92.0,10.0,10.0,10.0,10.0,10.0,8.0,7,1.53 +27414,,,,,,,,0, +72245,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.57 +33322,,,,,,,,0, +42340,,,,,,,,1,0.26 +1683,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.14 +6761,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.48 +63156,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.3 +46002,89.0,9.0,7.0,9.0,10.0,10.0,9.0,11,2.56 +13252,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.93 +43685,,,,,,,,0, +3970,87.0,9.0,9.0,5.0,7.0,9.0,9.0,3,0.87 +40791,80.0,9.0,9.0,8.0,10.0,10.0,9.0,4,1.58 +14346,100.0,10.0,10.0,10.0,8.0,9.0,10.0,3,1.17 +36651,20.0,4.0,4.0,6.0,6.0,4.0,2.0,1,0.61 +4000,84.0,9.0,9.0,9.0,9.0,9.0,9.0,11,3.98 +19330,,,,,,,,0, +41515,,,,,,,,1,0.22 +34210,20.0,2.0,2.0,2.0,2.0,2.0,2.0,1,0.25 +42835,70.0,6.0,5.0,10.0,10.0,10.0,7.0,2,0.79 +59820,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +61671,,,,,,,,0, +48169,,,,,,,,0, +59478,50.0,5.0,5.0,9.0,6.0,4.0,5.0,2,0.48 +36438,80.0,9.0,10.0,8.0,9.0,10.0,9.0,4,0.94 +28469,65.0,6.0,7.0,9.0,10.0,6.0,6.0,4,1.01 +71544,,,,,,,,0, +49961,80.0,8.0,8.0,6.0,8.0,8.0,9.0,2,0.51 +50519,,,,,,,,0, +11500,70.0,9.0,7.0,10.0,10.0,10.0,10.0,2,0.51 +50032,,,,,,,,0, +13898,94.0,9.0,9.0,10.0,10.0,10.0,10.0,10,2.27 +54232,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.22 +59310,100.0,10.0,8.0,8.0,10.0,8.0,8.0,1,0.39 +70010,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,0.99 +51550,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,1.68 +47671,100.0,10.0,6.0,10.0,8.0,10.0,10.0,1,0.47 +70310,,,,,,,,0, +58551,83.0,9.0,8.0,9.0,9.0,9.0,7.0,6,2.05 +25912,,,,,,,,0, +49833,100.0,10.0,9.0,10.0,10.0,7.0,9.0,3,1.23 +44311,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +68086,95.0,9.0,10.0,10.0,9.0,10.0,10.0,4,1.03 +53195,93.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.55 +67438,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.35 +1167,86.0,10.0,8.0,10.0,8.0,10.0,8.0,10,2.68 +72694,,,,,,,,0, +65788,92.0,10.0,9.0,10.0,10.0,10.0,10.0,21,4.88 +43334,100.0,10.0,10.0,10.0,10.0,9.0,9.0,6,1.57 +70730,,,,,,,,2,0.7 +38604,100.0,10.0,10.0,8.0,10.0,10.0,10.0,2,0.45 +38656,100.0,10.0,10.0,10.0,8.0,10.0,10.0,1,0.48 +42981,,,,,,,,0, +46293,73.0,7.0,6.0,9.0,10.0,9.0,7.0,3,0.68 +4417,90.0,10.0,10.0,9.0,10.0,9.0,9.0,4,0.99 +75204,83.0,8.0,8.0,8.0,8.0,9.0,8.0,7,2.12 +38011,,,,,,,,0, +76828,,,,,,,,0, +71828,,,,,,,,0, +424,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.57 +8796,95.0,10.0,9.0,10.0,10.0,9.0,9.0,17,5.73 +18614,73.0,8.0,7.0,5.0,7.0,9.0,7.0,3,0.78 +67450,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.3 +46013,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.45 +22578,100.0,8.0,8.0,8.0,8.0,10.0,10.0,1,0.33 +67685,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.46 +17149,90.0,10.0,9.0,8.0,10.0,10.0,9.0,2,0.72 +62881,,,,,,,,0, +52502,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.74 +27205,98.0,10.0,10.0,10.0,10.0,10.0,10.0,11,3.71 +24176,80.0,8.0,8.0,8.0,6.0,8.0,8.0,5,1.27 +67583,90.0,9.0,9.0,10.0,10.0,10.0,9.0,6,1.41 +15956,95.0,10.0,9.0,10.0,10.0,9.0,10.0,11,2.68 +2984,95.0,10.0,10.0,10.0,10.0,10.0,10.0,13,3.02 +18272,87.0,8.0,8.0,9.0,9.0,8.0,9.0,6,1.5 +62733,96.0,10.0,10.0,10.0,10.0,10.0,10.0,15,3.6 +9406,,,,,,,,0, +48099,90.0,9.0,9.0,9.0,9.0,9.0,9.0,41,9.32 +23740,87.0,9.0,9.0,10.0,9.0,10.0,9.0,6,1.45 +58576,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,0.77 +2081,85.0,9.0,9.0,9.0,9.0,10.0,9.0,36,9.08 +14587,84.0,9.0,8.0,9.0,9.0,9.0,9.0,26,5.86 +16674,86.0,9.0,8.0,9.0,9.0,10.0,9.0,25,5.81 +58333,82.0,9.0,8.0,9.0,9.0,9.0,9.0,18,4.39 +8123,,,,,,,,0, +10620,91.0,9.0,10.0,10.0,10.0,10.0,9.0,21,5.53 +46400,60.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +29224,90.0,8.0,10.0,9.0,9.0,9.0,9.0,4,2.11 +21422,,,,,,,,0, +3128,,,,,,,,0, +65816,87.0,8.0,10.0,7.0,9.0,10.0,9.0,14,3.56 +46529,,,,,,,,0, +10352,97.0,10.0,9.0,10.0,10.0,9.0,9.0,14,3.93 +52153,,,,,,,,0, +76177,90.0,10.0,9.0,10.0,10.0,9.0,10.0,4,1.15 +63675,,,,,,,,0, +51581,,,,,,,,0, +66658,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.94 +2634,87.0,9.0,8.0,10.0,10.0,10.0,9.0,6,1.58 +28640,,,,,,,,0, +69184,,,,,,,,0, +63959,,,,,,,,1,0.25 +2366,88.0,9.0,9.0,10.0,10.0,9.0,10.0,21,4.96 +63337,94.0,10.0,10.0,9.0,10.0,10.0,9.0,10,2.7 +53194,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.88 +75402,,,,,,,,0, +20652,,,,,,,,0, +41537,100.0,10.0,9.0,10.0,10.0,10.0,8.0,3,0.82 +72364,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +65706,80.0,9.0,10.0,10.0,10.0,8.0,8.0,2,0.51 +6447,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.27 +7999,80.0,10.0,10.0,6.0,6.0,10.0,10.0,2,0.56 +71181,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.76 +7070,90.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.76 +38974,93.0,10.0,9.0,10.0,10.0,10.0,9.0,3,1.25 +1327,96.0,9.0,10.0,9.0,10.0,10.0,9.0,11,2.92 +67916,60.0,7.0,7.0,8.0,9.0,8.0,5.0,2,0.48 +46087,,,,,,,,0, +63986,95.0,10.0,10.0,10.0,10.0,10.0,10.0,8,2.64 +32192,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,2.35 +2683,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,1.8 +52728,86.0,9.0,9.0,9.0,9.0,9.0,8.0,24,5.85 +50327,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,2.9 +13050,87.0,9.0,10.0,10.0,10.0,10.0,8.0,69,16.56 +11619,92.0,10.0,9.0,10.0,10.0,10.0,10.0,13,3.2 +10697,85.0,9.0,9.0,10.0,10.0,10.0,9.0,13,3.2 +47708,98.0,10.0,10.0,10.0,10.0,9.0,10.0,13,4.43 +25606,,,,,,,,0, +41108,100.0,10.0,10.0,10.0,10.0,10.0,10.0,18,4.58 +59995,96.0,10.0,9.0,10.0,10.0,10.0,9.0,10,2.88 +1241,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,1.85 +15006,92.0,9.0,9.0,8.0,9.0,10.0,9.0,5,1.39 +56670,90.0,8.0,8.0,9.0,8.0,9.0,8.0,3,1.96 +29926,97.0,10.0,10.0,10.0,10.0,10.0,10.0,15,3.88 +8182,96.0,10.0,9.0,9.0,10.0,9.0,10.0,24,6.21 +47408,78.0,9.0,8.0,10.0,10.0,9.0,8.0,13,3.45 +67975,85.0,8.0,9.0,10.0,10.0,10.0,9.0,4,0.97 +18920,97.0,10.0,9.0,10.0,10.0,9.0,9.0,7,2.04 +9201,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,1.78 +71383,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.52 +5282,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.26 +45537,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.59 +74027,98.0,10.0,10.0,10.0,10.0,9.0,10.0,13,3.64 +68008,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.76 +52514,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.27 +40137,92.0,9.0,10.0,10.0,9.0,9.0,9.0,10,2.8 +76144,94.0,10.0,9.0,10.0,10.0,10.0,10.0,7,1.79 +51516,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.04 +61322,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.51 +6461,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,2.29 +11475,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.26 +40926,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +68436,,,,,,,,0, +69195,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.64 +58753,,,,,,,,0, +37783,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.29 +10294,,,,,,,,0, +12853,98.0,10.0,9.0,10.0,10.0,10.0,10.0,16,5.33 +7686,74.0,9.0,8.0,10.0,9.0,9.0,8.0,10,2.78 +31503,,,,,,,,0, +6399,85.0,10.0,9.0,10.0,10.0,9.0,8.0,13,3.82 +68459,93.0,9.0,9.0,9.0,10.0,8.0,9.0,11,3.17 +24588,80.0,10.0,9.0,10.0,10.0,8.0,9.0,2,0.59 +65590,,,,,,,,0, +14538,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.03 +31109,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +27198,80.0,10.0,6.0,8.0,10.0,9.0,7.0,3,0.78 +24708,95.0,10.0,10.0,10.0,10.0,10.0,9.0,11,2.92 +34633,60.0,10.0,2.0,10.0,10.0,10.0,10.0,1,0.25 +55161,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,0.98 +41189,,,,,,,,0, +63714,80.0,9.0,9.0,10.0,9.0,9.0,8.0,10,2.75 +37002,92.0,10.0,9.0,10.0,10.0,9.0,10.0,23,8.12 +69657,91.0,10.0,10.0,10.0,10.0,10.0,9.0,13,3.31 +41867,,,,,,,,2,0.47 +29001,86.0,9.0,9.0,9.0,10.0,10.0,9.0,22,5.5 +52433,,,,,,,,0, +77056,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.52 +63477,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.25 +68668,,,,,,,,1,0.91 +75262,80.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.26 +15719,94.0,10.0,10.0,9.0,9.0,10.0,9.0,8,2.09 +37655,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,1.27 +66899,,,,,,,,1,0.64 +76574,,,,,,,,0, +63819,86.0,10.0,8.0,10.0,10.0,9.0,9.0,25,6.25 +29619,90.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.56 +59222,,,,,,,,0, +46132,80.0,9.0,8.0,10.0,9.0,9.0,9.0,6,1.64 +37696,91.0,10.0,9.0,10.0,10.0,9.0,10.0,30,7.56 +2002,73.0,8.0,9.0,9.0,10.0,9.0,7.0,3,0.76 +27719,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.28 +51666,93.0,10.0,10.0,10.0,9.0,10.0,10.0,6,1.61 +67684,86.0,9.0,9.0,10.0,10.0,9.0,9.0,41,9.69 +35174,,,,,,,,0, +72682,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.58 +70349,91.0,10.0,8.0,10.0,10.0,9.0,9.0,18,4.32 +2287,,,,,,,,0, +60997,80.0,8.0,6.0,8.0,10.0,10.0,7.0,3,0.83 +51916,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +33152,,,,,,,,0, +35114,97.0,10.0,10.0,9.0,10.0,10.0,10.0,31,8.02 +9865,,,,,,,,0, +61795,80.0,8.0,9.0,10.0,9.0,9.0,8.0,5,1.25 +1092,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.44 +18582,95.0,10.0,9.0,10.0,10.0,10.0,10.0,8,2.76 +44847,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,3.47 +17006,91.0,9.0,9.0,10.0,10.0,10.0,9.0,7,2.36 +18728,,,,,,,,0, +20438,,,,,,,,0, +5742,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.25 +61361,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.26 +116,,,,,,,,0, +60148,,,,,,,,0, +20196,,,,,,,,0, +16254,,,,,,,,0, +45679,,,,,,,,0, +70152,,,,,,,,0, +15548,,,,,,,,0, +34347,83.0,9.0,7.0,10.0,10.0,10.0,8.0,7,1.78 +35000,90.0,10.0,10.0,9.0,10.0,10.0,9.0,10,2.7 +46893,100.0,10.0,10.0,7.0,8.0,10.0,8.0,2,0.59 +32370,70.0,8.0,9.0,6.0,8.0,10.0,8.0,4,1.12 +74523,,,,,,,,0, +24900,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.71 +63466,70.0,8.0,6.0,10.0,9.0,8.0,7.0,2,0.73 +73742,100.0,10.0,10.0,10.0,10.0,10.0,10.0,16,4.71 +63674,,,,,,,,0, +24663,,,,,,,,0, +55831,87.0,10.0,9.0,10.0,10.0,9.0,9.0,27,6.53 +37753,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.45 +39303,,,,,,,,1,0.39 +39623,95.0,10.0,9.0,10.0,10.0,9.0,9.0,30,7.2 +28112,,,,,,,,0, +31899,90.0,10.0,9.0,10.0,10.0,9.0,9.0,24,5.9 +38521,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.02 +3509,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,0.99 +35845,90.0,9.0,9.0,10.0,10.0,9.0,9.0,2,0.86 +30672,95.0,10.0,9.0,10.0,10.0,9.0,10.0,13,5.49 +63461,96.0,10.0,9.0,10.0,10.0,9.0,9.0,18,4.32 +13641,,,,,,,,0, +5405,,,,,,,,0, +47645,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.63 +27269,,,,,,,,0, +40785,,,,,,,,1,0.36 +4841,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,1.07 +34548,80.0,9.0,7.0,8.0,8.0,9.0,9.0,4,1.02 +35075,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,4.15 +72468,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.27 +32299,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.26 +75498,,,,,,,,0, +10726,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.3 +39333,80.0,8.0,8.0,9.0,9.0,9.0,8.0,6,3.33 +109,92.0,10.0,10.0,10.0,10.0,10.0,10.0,10,2.52 +18647,89.0,9.0,9.0,9.0,10.0,10.0,9.0,15,5.17 +25782,80.0,9.0,8.0,9.0,9.0,9.0,8.0,11,5.59 +22218,84.0,9.0,8.0,10.0,9.0,9.0,8.0,16,7.27 +73309,85.0,9.0,9.0,10.0,10.0,9.0,8.0,13,5.82 +55221,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.31 +57132,85.0,10.0,10.0,10.0,10.0,10.0,9.0,4,1.29 +10033,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,2.0 +22483,100.0,10.0,10.0,10.0,10.0,7.0,10.0,2,0.48 +18208,100.0,8.0,8.0,8.0,10.0,8.0,10.0,1,0.29 +53385,97.0,10.0,10.0,10.0,10.0,10.0,9.0,6,1.59 +47031,95.0,10.0,9.0,10.0,10.0,9.0,10.0,22,6.88 +52774,,,,,,,,0, +28988,98.0,10.0,10.0,10.0,10.0,9.0,10.0,8,2.09 +61302,99.0,10.0,10.0,9.0,9.0,10.0,10.0,14,4.94 +60739,100.0,10.0,10.0,8.0,10.0,10.0,10.0,10,3.13 +76787,,,,,,,,0, +18162,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.29 +57330,99.0,10.0,10.0,10.0,10.0,9.0,10.0,15,4.64 +61162,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,3.31 +34525,93.0,10.0,10.0,9.0,10.0,10.0,9.0,3,1.36 +64308,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.06 +19983,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.04 +51082,90.0,9.0,8.0,10.0,10.0,7.0,8.0,2,0.98 +42848,,,,,,,,0, +39835,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,1.2 +555,86.0,9.0,9.0,9.0,10.0,10.0,8.0,14,5.0 +66734,97.0,10.0,9.0,10.0,10.0,10.0,10.0,7,1.86 +32937,,,,,,,,0, +47005,97.0,9.0,10.0,9.0,10.0,10.0,10.0,7,2.08 +58521,80.0,8.0,8.0,10.0,10.0,10.0,8.0,2,0.85 +73809,78.0,8.0,9.0,9.0,9.0,9.0,8.0,16,3.97 +21745,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,1.14 +39594,100.0,9.0,9.0,9.0,9.0,10.0,10.0,3,0.75 +49206,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,0.9 +49339,,,,,,,,0, +43894,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.67 +65223,,,,,,,,0, +46436,,,,,,,,0, +28515,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.33 +58418,100.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.51 +76751,97.0,9.0,9.0,10.0,10.0,10.0,9.0,7,2.02 +12354,,,,,,,,0, +70396,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.33 +64951,,,,,,,,0, +74223,100.0,10.0,10.0,10.0,9.0,9.0,9.0,5,1.53 +42486,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.3 +38890,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.28 +17338,,,,,,,,0, +3824,85.0,9.0,9.0,9.0,10.0,8.0,9.0,8,2.33 +63688,60.0,4.0,4.0,4.0,4.0,6.0,6.0,1,0.3 +37929,93.0,10.0,10.0,10.0,10.0,10.0,9.0,3,1.02 +26146,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,2.45 +47111,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.33 +43550,92.0,10.0,9.0,10.0,10.0,10.0,9.0,20,5.41 +40207,96.0,10.0,10.0,9.0,9.0,9.0,9.0,14,3.82 +76867,60.0,6.0,6.0,4.0,6.0,10.0,8.0,1,0.3 +56380,,,,,,,,0, +55837,93.0,9.0,9.0,9.0,9.0,9.0,9.0,3,0.83 +17596,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,2.17 +25372,96.0,10.0,10.0,10.0,10.0,10.0,9.0,5,1.22 +62884,80.0,9.0,5.0,10.0,9.0,10.0,8.0,3,0.88 +12209,100.0,10.0,10.0,8.0,10.0,10.0,10.0,2,0.54 +36694,95.0,10.0,10.0,9.0,10.0,10.0,9.0,4,1.15 +41626,,,,,,,,0, +22194,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,2.03 +10236,100.0,10.0,10.0,10.0,10.0,10.0,10.0,15,5.06 +52849,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,1.25 +75643,,,,,,,,0, +1891,,,,,,,,0, +18229,,,,,,,,0, +74724,90.0,10.0,10.0,10.0,9.0,9.0,9.0,8,2.47 +9267,97.0,10.0,9.0,10.0,10.0,10.0,10.0,6,2.17 +2383,,,,,,,,0, +22121,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.76 +27870,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,6.0 +50678,,,,,,,,1,0.24 +70946,,,,,,,,0, +13406,85.0,9.0,9.0,10.0,10.0,10.0,9.0,19,8.64 +26131,78.0,9.0,8.0,10.0,10.0,10.0,8.0,8,3.93 +37433,,,,,,,,0, +118,95.0,10.0,10.0,9.0,10.0,10.0,9.0,8,2.4 +355,,,,,,,,0, +70849,,,,,,,,0, +60603,,,,,,,,1,0.32 +53046,,,,,,,,0, +58079,,,,,,,,0, +69289,,,,,,,,0, +70596,80.0,10.0,8.0,8.0,10.0,8.0,6.0,1,0.26 +72903,83.0,9.0,8.0,10.0,10.0,10.0,8.0,8,4.29 +11714,,,,,,,,0, +74042,89.0,9.0,6.0,8.0,9.0,10.0,9.0,9,2.81 +58290,,,,,,,,0, +65914,73.0,7.0,9.0,10.0,7.0,9.0,9.0,5,2.68 +1878,,,,,,,,0, +77050,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.48 +21008,,,,,,,,0, +59594,,,,,,,,0, +18700,,,,,,,,0, +30881,,,,,,,,1, +12747,100.0,10.0,10.0,10.0,9.0,10.0,10.0,2,1.13 +45123,98.0,10.0,10.0,9.0,10.0,10.0,9.0,12,4.34 +74057,96.0,10.0,10.0,9.0,10.0,10.0,9.0,11,3.33 +25437,,,,,,,,0, +54093,84.0,8.0,8.0,8.0,9.0,10.0,8.0,10,3.26 +35980,,,,,,,,0, +27222,100.0,9.0,10.0,10.0,10.0,10.0,9.0,2,1.71 +76847,93.0,9.0,10.0,10.0,10.0,9.0,10.0,4,1.13 +45447,,,,,,,,0, +39107,,,,,,,,0, +28883,,,,,,,,0, +37630,,,,,,,,0, +48995,,,,,,,,0, +8449,,,,,,,,0, +37836,,,,,,,,0, +46349,,,,,,,,0, +57905,100.0,10.0,10.0,10.0,9.0,10.0,10.0,4,1.33 +39138,87.0,9.0,9.0,10.0,10.0,9.0,9.0,19,8.38 +54184,,,,,,,,1,0.26 +28982,60.0,5.0,7.0,9.0,7.0,7.0,5.0,6,3.21 +46863,,,,,,,,1, +38450,,,,,,,,0, +37482,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.55 +61426,100.0,10.0,10.0,10.0,10.0,10.0,9.0,4,1.19 +69898,,,,,,,,0, +11517,,,,,,,,0, +206,,,,,,,,0, +19936,,,,,,,,0, +3643,93.0,10.0,9.0,10.0,9.0,9.0,9.0,11,3.4 +13639,93.0,10.0,9.0,9.0,10.0,9.0,9.0,3,3.0 +73110,98.0,10.0,10.0,10.0,10.0,10.0,9.0,26,6.61 +61220,,,,,,,,0, +23648,,,,,,,,0, +46223,,,,,,,,0, +4913,,,,,,,,0, +34466,,,,,,,,0, +70373,,,,,,,,0, +37549,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.15 +40758,,,,,,,,0, +68053,93.0,10.0,10.0,10.0,10.0,10.0,10.0,11,2.92 +33395,,,,,,,,0, +67307,89.0,9.0,8.0,9.0,9.0,9.0,9.0,7,2.12 +23649,,,,,,,,0, +66763,96.0,10.0,10.0,9.0,10.0,10.0,8.0,5,1.95 +35673,80.0,8.0,8.0,10.0,10.0,10.0,8.0,2,0.67 +44694,92.0,10.0,9.0,9.0,10.0,10.0,10.0,26,7.16 +29696,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,1.0 +23206,85.0,10.0,10.0,10.0,9.0,10.0,9.0,8,2.82 +52209,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.34 +4482,85.0,9.0,9.0,9.0,9.0,10.0,8.0,16,4.07 +43280,90.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.17 +17497,82.0,8.0,7.0,10.0,9.0,9.0,8.0,9,2.65 +52268,100.0,9.0,8.0,10.0,10.0,10.0,9.0,4,1.35 +40422,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,1.05 +65118,100.0,8.0,10.0,10.0,6.0,10.0,10.0,1,0.45 +49528,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.72 +75357,100.0,9.0,9.0,10.0,10.0,10.0,9.0,3,0.8 +62823,,,,,,,,1,0.29 +4616,90.0,10.0,10.0,9.0,9.0,10.0,9.0,5,1.83 +213,,,,,,,,0, +10615,,,,,,,,0, +68293,,,,,,,,0, +28579,,,,,,,,0, +8918,,,,,,,,0, +54027,98.0,10.0,9.0,10.0,10.0,9.0,10.0,12,3.3 +46218,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.25 +34686,,,,,,,,0, +9601,94.0,10.0,9.0,10.0,10.0,10.0,10.0,21,5.34 +48445,,,,,,,,0, +47361,,,,,,,,0, +22239,70.0,8.0,7.0,7.0,8.0,6.0,6.0,4,1.14 +36708,,,,,,,,0, +10427,,,,,,,,0, +11629,,,,,,,,0, +13232,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.57 +58460,,,,,,,,0, +4667,70.0,8.0,10.0,10.0,10.0,10.0,6.0,4,1.67 +73020,93.0,10.0,10.0,10.0,10.0,9.0,9.0,3,1.0 +18038,100.0,10.0,10.0,10.0,10.0,9.0,10.0,10,2.91 +5164,93.0,10.0,9.0,10.0,9.0,10.0,9.0,11,2.84 +40922,,,,,,,,0, +36380,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.39 +48933,,,,,,,,0, +38217,88.0,9.0,9.0,10.0,10.0,10.0,9.0,13,3.42 +32516,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.28 +58742,100.0,10.0,10.0,10.0,10.0,6.0,10.0,2,0.52 +59709,,,,,,,,0, +62599,100.0,10.0,9.0,10.0,10.0,9.0,9.0,3,1.67 +51257,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,3.63 +32013,93.0,9.0,10.0,9.0,10.0,10.0,9.0,14,4.77 +61119,96.0,10.0,10.0,10.0,10.0,10.0,9.0,19,5.64 +70083,100.0,10.0,10.0,10.0,10.0,8.0,10.0,3,0.82 +63267,,,,,,,,0, +40453,90.0,9.0,9.0,6.0,7.0,9.0,8.0,2,0.67 +41742,100.0,10.0,9.0,10.0,10.0,10.0,9.0,3,1.08 +73163,,,,,,,,0, +49538,,,,,,,,0, +59836,100.0,10.0,10.0,9.0,9.0,9.0,9.0,3,1.08 +12374,100.0,10.0,9.0,10.0,9.0,10.0,9.0,5,1.61 +65746,,,,,,,,0, +47712,,,,,,,,0, +13059,,,,,,,,0, +41567,94.0,9.0,9.0,10.0,10.0,9.0,9.0,7,2.5 +50461,,,,,,,,0, +36141,88.0,10.0,9.0,10.0,10.0,9.0,9.0,5,1.55 +20726,93.0,10.0,9.0,10.0,10.0,9.0,9.0,3,1.1 +38009,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.28 +20657,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.29 +48693,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.67 +21713,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.63 +44523,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.49 +35794,93.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.61 +51729,,,,,,,,0, +5804,,,,,,,,0, +76492,79.0,9.0,8.0,8.0,8.0,9.0,9.0,20,5.61 +7930,,,,,,,,0, +75221,,,,,,,,0, +28599,94.0,9.0,10.0,10.0,9.0,10.0,9.0,7,2.63 +48428,73.0,9.0,7.0,7.0,8.0,9.0,8.0,3,1.48 +26903,89.0,9.0,9.0,9.0,9.0,9.0,9.0,11,3.63 +35423,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,2.37 +13818,,,,,,,,0, +56837,93.0,10.0,9.0,10.0,10.0,10.0,10.0,8,4.07 +11395,,,,,,,,1,0.79 +6880,73.0,9.0,7.0,7.0,7.0,8.0,9.0,3,0.83 +61204,90.0,10.0,9.0,9.0,10.0,9.0,9.0,5,1.35 +10762,,,,,,,,0, +67085,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.25 +48110,,,,,,,,0, +5239,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.48 +40460,,,,,,,,0, +69606,100.0,10.0,10.0,9.0,10.0,10.0,9.0,3,1.0 +5537,100.0,8.0,10.0,10.0,10.0,6.0,8.0,1,0.48 +28524,90.0,10.0,9.0,10.0,10.0,9.0,9.0,4,1.25 +69886,90.0,8.0,9.0,9.0,10.0,10.0,9.0,4,1.19 +39155,98.0,10.0,10.0,10.0,10.0,9.0,10.0,11,2.87 +49312,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,1.1 +68367,,,,,,,,0, +68467,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.08 +42,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.61 +15213,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,3.05 +56488,94.0,10.0,10.0,10.0,10.0,10.0,10.0,16,4.4 +76741,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,1.09 +16775,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.93 +56746,98.0,10.0,9.0,10.0,10.0,10.0,10.0,16,4.8 +13642,93.0,10.0,9.0,10.0,10.0,10.0,10.0,9,2.57 +10636,82.0,9.0,8.0,8.0,8.0,10.0,8.0,10,3.33 +18362,,,,,,,,0, +62698,94.0,9.0,10.0,10.0,10.0,10.0,9.0,7,2.92 +22540,85.0,9.0,9.0,8.0,9.0,10.0,9.0,8,2.24 +67993,100.0,10.0,8.0,10.0,10.0,8.0,10.0,1,0.41 +5842,99.0,10.0,10.0,10.0,10.0,10.0,10.0,25,6.94 +33656,97.0,10.0,10.0,10.0,10.0,10.0,10.0,28,7.37 +67855,93.0,9.0,9.0,10.0,10.0,9.0,9.0,10,2.68 +48275,,,,,,,,0, +51618,90.0,10.0,9.0,10.0,10.0,10.0,10.0,3,0.96 +1993,100.0,10.0,9.0,10.0,10.0,10.0,10.0,6,2.47 +24104,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.62 +20194,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.41 +55811,100.0,10.0,10.0,10.0,9.0,10.0,9.0,7,2.08 +19005,95.0,10.0,10.0,9.0,9.0,9.0,9.0,4,1.76 +23059,100.0,10.0,9.0,10.0,10.0,9.0,10.0,7,2.8 +54457,80.0,9.0,9.0,8.0,9.0,9.0,8.0,3,2.57 +70837,100.0,10.0,8.0,9.0,10.0,10.0,10.0,4,1.11 +15951,94.0,10.0,10.0,9.0,10.0,10.0,9.0,13,3.86 +4822,100.0,10.0,9.0,10.0,10.0,10.0,9.0,7,2.33 +41010,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,0.88 +62419,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.82 +55885,84.0,9.0,7.0,10.0,10.0,10.0,9.0,9,3.51 +59504,100.0,10.0,10.0,6.0,10.0,10.0,10.0,1,0.63 +57395,86.0,9.0,9.0,10.0,10.0,9.0,9.0,7,1.93 +44859,,,,,,,,0, +18544,93.0,9.0,10.0,10.0,10.0,10.0,9.0,12,3.91 +58699,97.0,10.0,9.0,10.0,10.0,10.0,9.0,6,1.73 +51480,92.0,9.0,10.0,10.0,9.0,10.0,9.0,10,2.7 +10132,,,,,,,,0, +76261,,,,,,,,0, +13679,100.0,9.0,9.0,10.0,10.0,9.0,9.0,3,1.58 +48678,,,,,,,,0, +20729,95.0,10.0,10.0,10.0,10.0,9.0,10.0,13,3.94 +21325,95.0,9.0,10.0,9.0,9.0,9.0,9.0,8,2.73 +49697,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.48 +51021,100.0,8.0,10.0,10.0,10.0,8.0,10.0,1,1.0 +6052,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +72202,94.0,9.0,10.0,10.0,10.0,9.0,10.0,13,4.43 +48697,20.0,2.0,2.0,2.0,2.0,2.0,2.0,2,0.54 +64925,98.0,10.0,10.0,10.0,10.0,10.0,10.0,10,3.61 +39295,80.0,9.0,7.0,9.0,9.0,10.0,9.0,7,2.16 +71615,100.0,10.0,10.0,10.0,10.0,9.0,10.0,8,2.76 +10552,91.0,9.0,10.0,9.0,9.0,8.0,9.0,7,2.14 +46903,84.0,10.0,9.0,10.0,10.0,9.0,9.0,9,2.73 +21636,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +19967,,,,,,,,0, +19787,98.0,10.0,10.0,10.0,10.0,9.0,10.0,21,7.0 +42630,80.0,8.0,9.0,9.0,9.0,10.0,7.0,7,3.0 +16880,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,0.98 +13948,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.36 +13799,,,,,,,,0, +16114,100.0,10.0,6.0,10.0,10.0,10.0,8.0,1,0.42 +33735,85.0,9.0,8.0,9.0,9.0,9.0,9.0,23,8.62 +30548,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.66 +68690,93.0,10.0,10.0,10.0,10.0,10.0,9.0,15,3.98 +30930,90.0,9.0,9.0,10.0,10.0,10.0,10.0,4,2.18 +75490,100.0,10.0,9.0,10.0,10.0,10.0,9.0,2,0.54 +912,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.83 +14432,,,,,,,,0, +62660,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.39 +38159,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.41 +40588,94.0,10.0,10.0,10.0,10.0,10.0,10.0,7,1.94 +24376,80.0,9.0,8.0,9.0,9.0,10.0,9.0,14,3.75 +19063,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.57 +26716,80.0,10.0,8.0,9.0,9.0,9.0,8.0,4,1.08 +7886,91.0,9.0,9.0,8.0,9.0,10.0,8.0,10,3.53 +24445,100.0,9.0,9.0,9.0,9.0,9.0,9.0,3,1.58 +17378,100.0,9.0,10.0,9.0,10.0,10.0,10.0,4,1.36 +33973,99.0,10.0,10.0,10.0,10.0,10.0,10.0,24,7.42 +28520,97.0,10.0,10.0,9.0,10.0,9.0,10.0,6,1.84 +68675,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.53 +49919,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +47620,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,1.2 +53717,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +50840,,,,,,,,1,0.52 +712,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.33 +32512,80.0,10.0,10.0,4.0,2.0,10.0,8.0,1,0.73 +24590,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.39 +69528,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +49017,,,,,,,,1,0.38 +62965,20.0,2.0,2.0,2.0,2.0,2.0,2.0,1,0.41 +17611,90.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.41 +66040,,,,,,,,0, +42886,80.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.11 +44508,80.0,6.0,8.0,10.0,8.0,8.0,8.0,1,0.36 +51520,,,,,,,,0, +26466,90.0,10.0,9.0,10.0,10.0,9.0,9.0,2,1.46 +7072,97.0,10.0,10.0,9.0,10.0,10.0,10.0,6,4.0 +19650,,,,,,,,0, +36205,,,,,,,,0, +12001,,,,,,,,0, +62892,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.42 +18864,80.0,10.0,8.0,10.0,8.0,10.0,10.0,1,0.79 +5672,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +35288,80.0,6.0,6.0,10.0,9.0,10.0,6.0,2,0.98 +51684,90.0,10.0,9.0,10.0,10.0,9.0,9.0,6,1.64 +5502,,,,,,,,0, +9964,89.0,9.0,8.0,10.0,9.0,9.0,9.0,7,3.89 +44352,85.0,9.0,9.0,10.0,9.0,9.0,8.0,12,5.54 +75826,,,,,,,,0, +71891,90.0,10.0,9.0,9.0,10.0,10.0,10.0,2,0.81 +1386,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,1.63 +9707,97.0,10.0,10.0,10.0,9.0,10.0,10.0,15,4.33 +23161,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.88 +43439,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.54 +35751,93.0,10.0,10.0,10.0,10.0,10.0,9.0,6,2.34 +67045,92.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.46 +9820,96.0,10.0,8.0,9.0,9.0,10.0,10.0,5,1.67 +44265,,,,,,,,0, +72922,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +36262,85.0,8.0,8.0,9.0,9.0,9.0,9.0,12,3.5 +38878,90.0,9.0,9.0,10.0,10.0,10.0,9.0,4,1.2 +62093,70.0,9.0,7.0,9.0,10.0,8.0,9.0,2,0.58 +32085,,,,,,,,0, +64205,90.0,10.0,10.0,10.0,10.0,9.0,10.0,8,2.47 +55567,,,,,,,,0, +74,80.0,10.0,10.0,10.0,9.0,10.0,9.0,4,1.4 +24087,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.56 +27512,94.0,9.0,9.0,9.0,9.0,9.0,9.0,14,3.93 +33138,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.48 +75406,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,1.55 +75458,,,,,,,,0, +23884,93.0,10.0,10.0,9.0,10.0,10.0,9.0,3,1.03 +47934,99.0,10.0,10.0,10.0,10.0,10.0,10.0,23,6.9 +39292,20.0,2.0,2.0,2.0,2.0,2.0,2.0,1,0.34 +51343,80.0,8.0,9.0,9.0,9.0,7.0,8.0,9,3.1 +33275,,,,,,,,0, +45691,,,,,,,,0, +53428,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.59 +33458,99.0,10.0,10.0,10.0,10.0,10.0,10.0,18,6.14 +70859,,,,,,,,0, +38014,90.0,10.0,10.0,10.0,9.0,10.0,10.0,4,1.38 +76268,,,,,,,,1, +15033,90.0,10.0,8.0,10.0,10.0,9.0,9.0,6,2.31 +72800,98.0,10.0,9.0,10.0,10.0,10.0,9.0,11,3.2 +51440,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,1.98 +37242,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,1.54 +68215,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,2.55 +29496,,,,,,,,0, +3887,92.0,10.0,8.0,10.0,10.0,10.0,10.0,33,10.88 +30345,85.0,9.0,9.0,8.0,7.0,8.0,8.0,14,4.77 +49290,97.0,10.0,10.0,10.0,10.0,10.0,10.0,19,5.48 +23907,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,0.91 +59552,91.0,9.0,8.0,9.0,9.0,10.0,9.0,7,2.44 +58829,97.0,10.0,10.0,10.0,10.0,10.0,9.0,7,3.23 +51357,84.0,8.0,9.0,9.0,9.0,10.0,8.0,9,3.51 +22626,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.38 +6068,80.0,9.0,7.0,9.0,9.0,10.0,8.0,4,1.45 +72691,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +1734,,,,,,,,1,1.0 +40706,93.0,10.0,9.0,10.0,10.0,9.0,9.0,3,1.45 +24714,97.0,10.0,10.0,9.0,10.0,9.0,10.0,6,2.57 +14356,60.0,6.0,6.0,4.0,6.0,10.0,6.0,1,0.45 +28684,,,,,,,,0, +7469,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.96 +73491,94.0,10.0,10.0,10.0,10.0,10.0,10.0,14,5.38 +54398,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,1.01 +76444,85.0,9.0,9.0,10.0,10.0,8.0,9.0,4,1.48 +1783,90.0,9.0,10.0,10.0,10.0,10.0,10.0,2,0.9 +56462,85.0,8.0,8.0,8.0,9.0,10.0,8.0,4,1.35 +24562,40.0,10.0,2.0,10.0,10.0,10.0,6.0,1,0.91 +71036,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,1.0 +2042,80.0,9.0,9.0,9.0,9.0,9.0,9.0,4,1.25 +8024,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,5.68 +67154,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.83 +71062,100.0,10.0,10.0,10.0,10.0,10.0,10.0,11,3.3 +66772,,,,,,,,0, +32326,97.0,10.0,9.0,10.0,9.0,10.0,10.0,6,2.73 +8986,90.0,10.0,9.0,9.0,10.0,9.0,9.0,10,3.61 +53281,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.39 +22352,,,,,,,,0, +23056,97.0,10.0,9.0,9.0,10.0,9.0,10.0,7,2.73 +11002,80.0,9.0,9.0,8.0,10.0,8.0,8.0,5,1.9 +31437,,,,,,,,0, +50367,,,,,,,,0, +5340,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.92 +26068,,,,,,,,0, +35923,98.0,10.0,10.0,10.0,10.0,8.0,9.0,13,3.82 +48947,93.0,10.0,9.0,9.0,9.0,9.0,9.0,3,1.43 +48118,96.0,10.0,10.0,10.0,10.0,10.0,9.0,15,5.56 +18083,95.0,10.0,10.0,10.0,10.0,10.0,10.0,23,8.12 +29994,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,0.63 +73957,,,,,,,,1, +67393,,,,,,,,0, +39357,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.62 +43173,94.0,10.0,9.0,9.0,9.0,9.0,9.0,7,2.56 +32007,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.38 +39693,,,,,,,,0, +4422,94.0,10.0,9.0,10.0,10.0,9.0,10.0,7,2.04 +57953,,,,,,,,0, +858,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,1.0 +428,,,,,,,,0, +27292,80.0,9.0,8.0,8.0,8.0,9.0,9.0,12,4.04 +40487,90.0,10.0,10.0,10.0,10.0,10.0,9.0,10,5.0 +39565,70.0,8.0,7.0,9.0,9.0,10.0,7.0,16,5.22 +61673,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.3 +32035,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,2.02 +36158,80.0,8.0,9.0,9.0,9.0,8.0,9.0,3,0.97 +51140,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +13150,,,,,,,,0, +63209,,,,,,,,1,0.59 +5432,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.38 +53122,85.0,10.0,9.0,10.0,10.0,9.0,9.0,4,1.67 +7828,95.0,10.0,9.0,10.0,10.0,9.0,10.0,13,5.0 +68232,80.0,9.0,7.0,9.0,9.0,10.0,8.0,2,0.78 +12072,80.0,9.0,6.0,8.0,10.0,9.0,8.0,2,0.75 +68698,90.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.97 +43346,,,,,,,,1, +35558,100.0,9.0,10.0,6.0,9.0,10.0,9.0,2,0.73 +12535,93.0,9.0,9.0,10.0,10.0,9.0,9.0,12,3.75 +15400,70.0,7.0,9.0,7.0,8.0,8.0,6.0,3,1.18 +58021,,,,,,,,0, +18048,83.0,10.0,9.0,10.0,10.0,10.0,8.0,7,4.04 +19208,90.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.65 +13065,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,2.76 +66035,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.56 +35959,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,1.0 +44961,,,,,,,,1,0.3 +40383,95.0,10.0,10.0,10.0,10.0,8.0,9.0,4,1.15 +25229,93.0,10.0,10.0,8.0,9.0,8.0,10.0,6,5.0 +62313,,,,,,,,0, +43539,80.0,10.0,9.0,8.0,9.0,10.0,9.0,4,1.17 +65701,67.0,5.0,9.0,5.0,5.0,7.0,5.0,3,1.0 +23151,,,,,,,,0, +31836,,,,,,,,0, +17158,95.0,10.0,10.0,10.0,10.0,10.0,9.0,12,5.54 +67699,88.0,8.0,9.0,9.0,10.0,10.0,9.0,13,6.84 +38252,89.0,10.0,9.0,10.0,10.0,10.0,10.0,17,5.2 +58048,74.0,8.0,6.0,8.0,8.0,9.0,8.0,10,3.13 +69680,,,,,,,,0, +13091,100.0,10.0,10.0,9.0,10.0,10.0,9.0,5,1.92 +17017,,,,,,,,0, +520,80.0,9.0,8.0,7.0,7.0,9.0,8.0,8,2.86 +52951,100.0,10.0,9.0,10.0,10.0,10.0,9.0,5,3.06 +11368,100.0,10.0,10.0,8.0,10.0,10.0,10.0,2,0.97 +23706,91.0,10.0,10.0,10.0,10.0,10.0,9.0,20,6.52 +44292,,,,,,,,0, +417,100.0,10.0,8.0,10.0,10.0,10.0,10.0,4,1.28 +58670,,,,,,,,0, +48905,97.0,9.0,10.0,10.0,10.0,8.0,10.0,7,2.69 +43394,80.0,6.0,8.0,10.0,10.0,10.0,8.0,1,1.0 +67995,87.0,9.0,9.0,9.0,10.0,10.0,10.0,3,0.92 +51812,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,1.55 +33521,88.0,9.0,8.0,9.0,9.0,9.0,8.0,10,4.0 +10698,80.0,8.0,6.0,4.0,8.0,10.0,8.0,2,1.09 +58267,96.0,10.0,8.0,10.0,10.0,8.0,8.0,5,1.7 +60065,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,2.0 +72157,,,,,,,,0, +15106,90.0,10.0,10.0,9.0,8.0,10.0,9.0,2,1.33 +60046,84.0,8.0,8.0,9.0,8.0,8.0,8.0,6,2.2 +63098,87.0,10.0,8.0,10.0,10.0,10.0,9.0,9,3.7 +71310,93.0,9.0,10.0,9.0,9.0,10.0,9.0,11,4.52 +10716,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.58 +63764,93.0,10.0,10.0,9.0,10.0,10.0,9.0,8,3.53 +46726,,,,,,,,0, +59617,97.0,10.0,10.0,10.0,10.0,10.0,9.0,6,2.17 +7325,,,,,,,,0, +32330,,,,,,,,0, +34139,93.0,9.0,10.0,10.0,10.0,9.0,9.0,9,2.97 +70047,,,,,,,,0, +28823,100.0,10.0,10.0,9.0,10.0,10.0,10.0,3,1.06 +32534,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.38 +29359,98.0,9.0,9.0,9.0,10.0,10.0,10.0,8,2.93 +35239,,,,,,,,0, +65593,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.24 +34364,90.0,10.0,9.0,10.0,10.0,10.0,10.0,2,0.94 +43036,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.4 +12143,80.0,8.0,10.0,10.0,8.0,10.0,10.0,1,0.38 +59649,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.43 +53091,100.0,10.0,10.0,10.0,9.0,10.0,10.0,2,0.63 +54009,90.0,10.0,9.0,10.0,10.0,10.0,9.0,6,2.65 +3362,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.71 +24479,100.0,10.0,10.0,9.0,10.0,10.0,9.0,2,0.67 +54394,96.0,10.0,10.0,10.0,10.0,9.0,10.0,5,1.9 +26596,,,,,,,,0, +21910,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.29 +39289,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.5 +44356,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,1.82 +34291,80.0,10.0,10.0,8.0,10.0,10.0,10.0,1,1.0 +76042,95.0,10.0,10.0,10.0,10.0,10.0,10.0,5,2.38 +16128,87.0,10.0,10.0,10.0,10.0,10.0,9.0,6,1.8 +29354,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,2.17 +50579,95.0,9.0,10.0,10.0,10.0,10.0,10.0,5,1.47 +55935,80.0,6.0,9.0,5.0,5.0,8.0,7.0,2,0.63 +76293,96.0,10.0,10.0,10.0,10.0,10.0,9.0,11,3.67 +53421,85.0,9.0,9.0,10.0,9.0,10.0,10.0,4,4.0 +37,55.0,8.0,8.0,9.0,6.0,9.0,7.0,5,1.95 +37775,100.0,10.0,9.0,10.0,10.0,10.0,10.0,5,1.67 +67382,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,1.0 +70903,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,1.54 +47604,,,,,,,,0, +10802,,,,,,,,0, +5707,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.55 +22550,93.0,9.0,9.0,9.0,9.0,9.0,9.0,13,4.15 +46886,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.43 +26327,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,3.0 +1687,,,,,,,,0, +73156,93.0,9.0,8.0,10.0,10.0,10.0,9.0,6,2.02 +58284,,,,,,,,1,0.45 +76624,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.33 +46840,,,,,,,,0, +25461,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.39 +29023,,,,,,,,0, +69639,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +74916,60.0,6.0,6.0,6.0,6.0,10.0,8.0,1,0.31 +12725,,,,,,,,0, +46368,,,,,,,,1,0.75 +17662,88.0,8.0,9.0,9.0,9.0,10.0,9.0,5,2.46 +25111,93.0,10.0,9.0,10.0,10.0,10.0,9.0,8,3.33 +62395,100.0,9.0,10.0,10.0,10.0,10.0,10.0,7,2.47 +41585,92.0,9.0,9.0,9.0,9.0,9.0,9.0,5,1.65 +47933,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.2 +4691,87.0,9.0,8.0,7.0,10.0,10.0,9.0,3,1.48 +52054,100.0,10.0,10.0,10.0,10.0,10.0,10.0,17,5.67 +45324,,,,,,,,0, +36697,100.0,10.0,10.0,10.0,10.0,9.0,10.0,12,4.04 +62529,96.0,10.0,10.0,10.0,10.0,10.0,10.0,32,9.8 +46583,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,1.74 +21317,92.0,9.0,10.0,9.0,8.0,10.0,9.0,5,2.08 +53160,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +3499,87.0,9.0,9.0,9.0,9.0,10.0,9.0,3,2.05 +9933,,,,,,,,0, +68372,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,1.22 +70772,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,2.97 +41343,96.0,10.0,10.0,10.0,10.0,10.0,8.0,5,1.95 +43689,100.0,9.0,10.0,10.0,10.0,9.0,9.0,2,0.88 +24458,95.0,10.0,10.0,10.0,10.0,9.0,9.0,4,2.5 +8042,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.15 +71268,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.42 +58197,,,,,,,,0, +31710,100.0,10.0,10.0,9.0,10.0,10.0,10.0,3,1.2 +63195,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,4.35 +19874,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,2.34 +49362,80.0,10.0,10.0,10.0,10.0,8.0,8.0,1,1.0 +5441,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,2.5 +53172,98.0,10.0,10.0,10.0,10.0,10.0,9.0,9,2.87 +58945,93.0,9.0,10.0,10.0,10.0,10.0,10.0,4,1.71 +25206,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.18 +35027,96.0,10.0,9.0,10.0,10.0,10.0,10.0,5,1.79 +51532,87.0,9.0,9.0,7.0,8.0,10.0,9.0,18,6.28 +62986,,,,,,,,0, +48903,100.0,10.0,10.0,9.0,9.0,10.0,9.0,4,1.82 +59391,97.0,10.0,9.0,10.0,10.0,10.0,10.0,15,5.17 +54338,90.0,10.0,9.0,10.0,10.0,9.0,9.0,2,0.85 +1674,88.0,9.0,8.0,8.0,10.0,9.0,9.0,5,2.46 +9281,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,2.05 +43574,90.0,9.0,9.0,10.0,10.0,10.0,9.0,10,3.41 +48433,93.0,9.0,10.0,10.0,10.0,10.0,9.0,3,1.8 +31352,100.0,10.0,10.0,9.0,9.0,10.0,9.0,3,1.02 +68569,85.0,10.0,9.0,10.0,10.0,9.0,9.0,8,3.93 +19648,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,2.0 +2658,93.0,9.0,9.0,10.0,10.0,9.0,9.0,3,1.61 +14294,86.0,9.0,10.0,10.0,10.0,9.0,9.0,10,4.41 +68285,,,,,,,,0, +41912,100.0,10.0,10.0,10.0,10.0,9.0,10.0,12,4.29 +51764,,,,,,,,0, +47931,,,,,,,,0, +45944,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,2.63 +19581,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +22263,,,,,,,,0, +70037,,,,,,,,0, +71803,100.0,10.0,9.0,9.0,10.0,10.0,9.0,2,0.69 +66437,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +38637,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.07 +37701,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.85 +5240,90.0,9.0,9.0,10.0,10.0,10.0,9.0,11,3.88 +70225,95.0,9.0,9.0,9.0,10.0,10.0,9.0,11,4.02 +35809,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.58 +49728,100.0,10.0,10.0,10.0,10.0,9.0,10.0,10,3.7 +36943,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,1.0 +8041,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,3.95 +55672,92.0,9.0,10.0,10.0,10.0,10.0,10.0,5,1.9 +23444,93.0,10.0,10.0,9.0,10.0,8.0,9.0,7,2.88 +51132,90.0,10.0,10.0,10.0,9.0,10.0,9.0,4,1.35 +63573,93.0,9.0,10.0,10.0,10.0,10.0,9.0,11,3.93 +29085,,,,,,,,0, +28790,,,,,,,,0, +10979,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.36 +15040,,,,,,,,0, +39573,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,2.82 +26816,100.0,9.0,10.0,10.0,10.0,10.0,9.0,2,1.3 +45660,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,1.0 +52560,,,,,,,,0, +11738,,,,,,,,0, +5162,91.0,10.0,9.0,10.0,10.0,10.0,9.0,9,3.07 +11534,87.0,9.0,9.0,9.0,9.0,9.0,9.0,19,6.63 +4991,,,,,,,,1, +13290,87.0,9.0,9.0,10.0,10.0,9.0,9.0,4,2.79 +35906,90.0,9.0,10.0,9.0,8.0,9.0,8.0,6,3.75 +67369,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,1.36 +55358,95.0,10.0,8.0,10.0,10.0,10.0,10.0,4,1.9 +42676,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.39 +65069,100.0,10.0,10.0,10.0,10.0,10.0,9.0,7,2.47 +62075,94.0,10.0,10.0,10.0,10.0,9.0,9.0,7,2.33 +52613,85.0,10.0,9.0,10.0,10.0,9.0,10.0,4,1.45 +10029,60.0,7.0,8.0,7.0,7.0,7.0,7.0,3,1.3 +73379,85.0,10.0,10.0,10.0,10.0,10.0,9.0,4,4.0 +63935,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.37 +21341,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.54 +7401,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.37 +7415,90.0,9.0,10.0,10.0,10.0,10.0,9.0,8,3.38 +75965,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,2.61 +13517,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.41 +59004,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.55 +74858,50.0,6.0,4.0,5.0,6.0,6.0,6.0,2,0.8 +34816,,,,,,,,1,0.32 +18680,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.22 +36362,,,,,,,,0, +66010,,,,,,,,0, +14068,87.0,9.0,9.0,9.0,10.0,9.0,8.0,6,2.43 +65501,80.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.53 +42132,97.0,10.0,10.0,10.0,9.0,10.0,9.0,7,3.28 +11703,,,,,,,,0, +59580,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.88 +42247,98.0,10.0,10.0,9.0,9.0,10.0,9.0,9,3.18 +37723,100.0,10.0,10.0,6.0,10.0,10.0,10.0,1,0.48 +69015,100.0,10.0,10.0,10.0,10.0,9.0,10.0,11,4.12 +45657,93.0,9.0,8.0,10.0,10.0,10.0,9.0,3,1.0 +50973,100.0,10.0,10.0,9.0,10.0,9.0,9.0,3,1.36 +22273,83.0,9.0,8.0,9.0,10.0,10.0,8.0,8,2.82 +25505,100.0,10.0,9.0,10.0,10.0,9.0,9.0,3,1.3 +26462,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +54395,93.0,10.0,9.0,10.0,10.0,10.0,9.0,3,1.58 +22728,,,,,,,,0, +11958,,,,,,,,0, +9520,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +58528,100.0,9.0,10.0,10.0,10.0,10.0,9.0,2,1.22 +51650,80.0,10.0,10.0,2.0,4.0,10.0,10.0,1,0.43 +24424,,,,,,,,0, +5308,100.0,10.0,10.0,10.0,10.0,8.0,9.0,2,0.66 +52006,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.4 +13359,,,,,,,,0, +14925,,,,,,,,0, +46901,98.0,10.0,10.0,10.0,10.0,9.0,10.0,20,6.67 +38951,87.0,9.0,8.0,10.0,10.0,9.0,9.0,3,1.48 +47840,,,,,,,,0, +10910,90.0,9.0,9.0,10.0,10.0,10.0,9.0,3,1.13 +61752,,,,,,,,0, +28949,,,,,,,,0, +7437,98.0,10.0,9.0,10.0,10.0,9.0,10.0,9,3.18 +34511,90.0,10.0,10.0,10.0,10.0,10.0,9.0,4,1.94 +23441,95.0,9.0,10.0,10.0,10.0,10.0,10.0,4,1.97 +36938,97.0,10.0,9.0,10.0,9.0,9.0,9.0,7,3.28 +71708,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,2.12 +57688,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.54 +62567,96.0,10.0,9.0,10.0,10.0,10.0,10.0,9,3.21 +3384,93.0,9.0,9.0,9.0,10.0,9.0,7.0,3,1.08 +67605,93.0,10.0,9.0,7.0,9.0,9.0,8.0,6,2.47 +65928,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.68 +28896,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.4 +16594,97.0,9.0,9.0,9.0,10.0,10.0,9.0,6,2.73 +38387,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.43 +51808,93.0,10.0,10.0,9.0,10.0,10.0,10.0,3,2.14 +53519,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.83 +51963,50.0,6.0,5.0,6.0,4.0,6.0,6.0,2,1.18 +62339,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +9120,,,,,,,,0, +60328,100.0,9.0,10.0,10.0,9.0,9.0,9.0,2,0.97 +25496,,,,,,,,3, +22685,89.0,9.0,10.0,10.0,10.0,9.0,9.0,13,4.33 +10662,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.32 +47007,93.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.02 +59109,90.0,9.0,9.0,9.0,9.0,9.0,9.0,10,5.26 +56283,100.0,10.0,9.0,9.0,10.0,10.0,10.0,6,2.4 +28866,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.42 +22287,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.47 +53623,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,1.96 +76306,90.0,9.0,10.0,10.0,10.0,10.0,9.0,4,1.54 +36565,93.0,9.0,9.0,8.0,10.0,9.0,9.0,3,1.61 +1912,84.0,8.0,10.0,8.0,8.0,10.0,8.0,5,1.85 +44254,,,,,,,,0, +31313,100.0,10.0,10.0,9.0,10.0,10.0,10.0,7,3.04 +40070,,,,,,,,0, +51288,95.0,10.0,9.0,10.0,10.0,10.0,9.0,5,2.05 +6527,87.0,10.0,10.0,10.0,10.0,9.0,9.0,3,1.7 +32560,,,,,,,,0, +30910,100.0,8.0,10.0,8.0,8.0,10.0,8.0,1,0.48 +31377,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +36137,70.0,8.0,9.0,7.0,6.0,8.0,8.0,2,0.76 +63878,85.0,10.0,10.0,10.0,9.0,8.0,9.0,8,2.86 +14316,,,,,,,,0, +67501,,,,,,,,0, +67249,,,,,,,,0, +72789,97.0,10.0,10.0,10.0,10.0,9.0,10.0,15,5.49 +65896,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.11 +57424,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.94 +52809,90.0,10.0,10.0,9.0,10.0,10.0,9.0,6,3.21 +43549,90.0,9.0,8.0,9.0,10.0,9.0,9.0,6,2.07 +43301,98.0,10.0,10.0,10.0,10.0,10.0,9.0,8,6.15 +66606,94.0,10.0,9.0,10.0,10.0,9.0,10.0,10,3.8 +46801,100.0,10.0,8.0,10.0,10.0,6.0,8.0,1,0.39 +75720,100.0,10.0,10.0,9.0,10.0,10.0,10.0,3,1.67 +33615,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.56 +51569,80.0,8.0,8.0,9.0,8.0,8.0,7.0,2,0.8 +68624,77.0,8.0,9.0,9.0,9.0,9.0,9.0,6,2.95 +57927,,,,,,,,0, +42528,98.0,10.0,9.0,10.0,10.0,9.0,10.0,8,3.38 +25727,87.0,9.0,10.0,10.0,10.0,10.0,10.0,3,2.25 +66391,98.0,10.0,10.0,9.0,10.0,10.0,10.0,9,4.22 +66129,84.0,9.0,10.0,8.0,8.0,8.0,8.0,5,2.42 +28962,,,,,,,,0, +10621,,,,,,,,1,0.39 +41350,86.0,9.0,9.0,9.0,10.0,10.0,9.0,7,2.92 +45274,80.0,6.0,10.0,10.0,4.0,10.0,10.0,1,0.65 +45198,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +39689,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,2.53 +37219,,,,,,,,0, +19020,60.0,5.0,4.0,10.0,6.0,7.0,7.0,3,1.13 +18534,90.0,8.0,9.0,10.0,9.0,9.0,9.0,2,1.4 +67410,73.0,9.0,9.0,9.0,7.0,10.0,9.0,3,1.18 +27817,90.0,10.0,9.0,10.0,10.0,9.0,8.0,8,3.69 +43003,87.0,8.0,8.0,9.0,10.0,10.0,7.0,6,2.17 +45172,,,,,,,,0, +36357,90.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.41 +53163,,,,,,,,0, +15644,93.0,10.0,9.0,10.0,10.0,10.0,9.0,18,6.67 +58596,88.0,10.0,10.0,10.0,10.0,9.0,9.0,8,2.7 +45484,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,1.0 +9475,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.73 +63221,,,,,,,,0, +71638,,,,,,,,0, +50000,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.13 +50050,95.0,10.0,10.0,9.0,10.0,10.0,10.0,8,3.53 +9653,,,,,,,,0, +31036,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,3.33 +14514,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +52739,89.0,9.0,10.0,9.0,10.0,9.0,9.0,7,2.63 +62848,,,,,,,,0, +33062,75.0,9.0,6.0,10.0,10.0,10.0,8.0,8,3.33 +15675,93.0,9.0,9.0,10.0,10.0,7.0,9.0,3,1.38 +11430,88.0,10.0,10.0,8.0,9.0,10.0,10.0,5,3.66 +24824,100.0,10.0,10.0,8.0,10.0,8.0,8.0,1,0.73 +35758,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,3.68 +14061,87.0,9.0,10.0,9.0,9.0,10.0,9.0,14,4.94 +53630,,,,,,,,0, +56143,,,,,,,,0, +22901,80.0,9.0,9.0,8.0,8.0,9.0,9.0,8,3.24 +31212,87.0,10.0,8.0,10.0,10.0,9.0,9.0,3,1.91 +76030,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.37 +54605,,,,,,,,0, +44639,70.0,7.0,6.0,6.0,9.0,8.0,8.0,2,0.88 +12912,73.0,9.0,7.0,8.0,10.0,9.0,8.0,3,2.65 +9016,90.0,10.0,10.0,10.0,9.0,9.0,9.0,3,1.41 +70479,85.0,9.0,8.0,9.0,9.0,10.0,8.0,4,1.58 +30224,,,,,,,,0, +14205,100.0,10.0,9.0,10.0,9.0,9.0,10.0,2,0.8 +56213,93.0,9.0,10.0,9.0,9.0,8.0,9.0,10,3.49 +73717,100.0,10.0,10.0,10.0,10.0,9.0,9.0,2,0.95 +49401,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,2.05 +27463,88.0,10.0,9.0,10.0,9.0,9.0,9.0,5,2.17 +8029,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.35 +39929,100.0,10.0,10.0,10.0,10.0,10.0,9.0,4,1.67 +30434,,,,,,,,0, +63194,97.0,9.0,9.0,10.0,10.0,10.0,9.0,7,2.69 +35061,,,,,,,,1,1.0 +59652,,,,,,,,1,0.41 +50851,96.0,10.0,9.0,9.0,10.0,10.0,9.0,11,4.18 +51547,70.0,10.0,8.0,10.0,8.0,10.0,9.0,4,2.79 +64644,98.0,10.0,9.0,10.0,10.0,10.0,10.0,8,3.24 +26037,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,1.18 +17721,,,,,,,,0, +66926,93.0,10.0,10.0,10.0,10.0,9.0,10.0,3,2.0 +28351,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,2.76 +63069,,,,,,,,0, +14794,83.0,9.0,8.0,10.0,9.0,9.0,9.0,8,4.53 +21542,,,,,,,,0, +69996,,,,,,,,1, +28259,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.72 +1956,,,,,,,,0, +51024,92.0,10.0,9.0,10.0,10.0,9.0,9.0,5,2.17 +44823,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,4.0 +58413,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.4 +50595,,,,,,,,0, +35931,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.53 +67184,,,,,,,,0, +51896,80.0,7.0,9.0,7.0,8.0,9.0,8.0,4,3.53 +4639,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +24915,,,,,,,,0, +28734,,,,,,,,0, +61947,,,,,,,,0, +41648,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.91 +71625,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.58 +68377,85.0,9.0,10.0,8.0,9.0,8.0,8.0,4,1.64 +23893,100.0,8.0,10.0,8.0,8.0,8.0,8.0,2,0.73 +57934,90.0,8.0,8.0,9.0,10.0,9.0,8.0,2,1.11 +1089,80.0,8.0,6.0,8.0,8.0,6.0,6.0,1,0.77 +74417,,,,,,,,0, +33731,,,,,,,,0, +38919,97.0,10.0,10.0,10.0,10.0,9.0,10.0,7,3.0 +49652,98.0,10.0,10.0,10.0,10.0,9.0,9.0,11,4.34 +3467,60.0,7.0,5.0,10.0,8.0,6.0,6.0,2,1.09 +55598,,,,,,,,0, +37467,94.0,10.0,9.0,10.0,10.0,10.0,10.0,14,5.32 +6982,97.0,10.0,9.0,10.0,10.0,10.0,9.0,6,2.65 +48800,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.63 +20642,100.0,10.0,10.0,10.0,10.0,8.0,9.0,3,1.23 +14826,,,,,,,,0, +7015,93.0,10.0,9.0,10.0,10.0,9.0,9.0,18,7.83 +11252,85.0,9.0,10.0,8.0,9.0,10.0,9.0,8,3.12 +13846,89.0,9.0,9.0,10.0,10.0,10.0,9.0,7,2.73 +70027,,,,,,,,0, +69216,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,2.76 +15043,80.0,8.0,10.0,10.0,9.0,10.0,8.0,3,1.22 +15265,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.61 +26258,100.0,9.0,10.0,9.0,10.0,9.0,9.0,2,1.25 +53634,,,,,,,,0, +53590,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,3.62 +29969,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,4.29 +51825,90.0,10.0,9.0,8.0,10.0,9.0,9.0,3,1.15 +36144,90.0,9.0,9.0,10.0,10.0,10.0,9.0,4,2.22 +45831,100.0,10.0,6.0,10.0,10.0,10.0,10.0,1,0.48 +28563,93.0,9.0,9.0,9.0,10.0,9.0,9.0,3,1.08 +57378,,,,,,,,0, +42103,82.0,8.0,7.0,8.0,9.0,9.0,8.0,9,4.09 +59831,72.0,8.0,7.0,9.0,8.0,9.0,7.0,10,3.8 +33284,100.0,10.0,7.0,10.0,10.0,10.0,10.0,2,0.72 +52684,92.0,10.0,9.0,9.0,10.0,9.0,8.0,5,2.17 +58044,95.0,10.0,10.0,10.0,10.0,9.0,10.0,5,2.34 +10331,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.43 +34051,97.0,9.0,9.0,10.0,9.0,9.0,9.0,6,2.73 +35070,,,,,,,,0, +67697,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,4.86 +58387,80.0,10.0,8.0,10.0,8.0,10.0,8.0,1,0.54 +38404,,,,,,,,1,0.57 +9273,96.0,10.0,10.0,10.0,10.0,9.0,9.0,11,5.24 +39179,,,,,,,,0, +29397,,,,,,,,0, +72077,60.0,8.0,10.0,8.0,6.0,8.0,8.0,2,0.87 +8800,,,,,,,,0, +16988,,,,,,,,0, +12226,,,,,,,,1,0.43 +45369,74.0,8.0,8.0,8.0,8.0,9.0,8.0,7,2.56 +72737,,,,,,,,0, +21041,80.0,9.0,8.0,9.0,9.0,7.0,8.0,3,1.15 +62716,91.0,9.0,8.0,10.0,10.0,10.0,9.0,7,2.76 +4089,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,0.88 +49218,100.0,10.0,10.0,10.0,10.0,10.0,9.0,9,3.55 +63662,,,,,,,,0, +64348,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.74 +37572,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +46907,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.58 +62642,,,,,,,,0, +78,74.0,8.0,7.0,9.0,9.0,9.0,8.0,16,6.4 +32971,87.0,10.0,9.0,9.0,10.0,10.0,9.0,9,4.22 +3653,100.0,9.0,9.0,9.0,9.0,9.0,9.0,2,0.79 +60114,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,1.29 +42134,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,2.43 +4863,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.8 +50523,100.0,10.0,10.0,10.0,10.0,9.0,10.0,10,4.69 +49143,,,,,,,,0, +50369,93.0,10.0,10.0,10.0,10.0,9.0,10.0,11,5.08 +66522,,,,,,,,0, +17940,92.0,10.0,10.0,9.0,8.0,8.0,9.0,5,2.68 +41514,93.0,9.0,9.0,10.0,10.0,10.0,9.0,3,1.58 +34261,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.97 +39522,,,,,,,,0, +70336,,,,,,,,0, +64350,,,,,,,,1,0.73 +3621,90.0,10.0,9.0,10.0,9.0,10.0,9.0,4,2.55 +3798,92.0,10.0,10.0,10.0,10.0,10.0,9.0,17,6.46 +57533,93.0,9.0,10.0,9.0,10.0,9.0,10.0,3,1.64 +41656,93.0,10.0,9.0,10.0,10.0,10.0,10.0,6,2.81 +74730,,,,,,,,0, +54626,,,,,,,,0, +40461,,,,,,,,0, +38210,93.0,10.0,10.0,10.0,9.0,10.0,10.0,9,4.35 +58913,,,,,,,,0, +46190,93.0,10.0,9.0,9.0,9.0,9.0,8.0,3,1.3 +18261,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.45 +12528,,,,,,,,0, +72750,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,3.0 +41960,85.0,9.0,9.0,9.0,8.0,9.0,9.0,12,4.62 +35270,95.0,10.0,10.0,10.0,10.0,9.0,10.0,12,5.29 +2739,80.0,8.0,7.0,10.0,9.0,8.0,7.0,4,2.26 +72107,100.0,10.0,10.0,9.0,10.0,10.0,10.0,3,1.34 +59475,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,2.96 +27986,93.0,7.0,9.0,10.0,10.0,10.0,9.0,3,1.17 +53348,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.79 +43578,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.02 +20166,,,,,,,,0, +29014,,,,,,,,0, +42116,,,,,,,,0, +43323,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.26 +23935,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,3.16 +55962,,,,,,,,0, +16406,90.0,10.0,10.0,10.0,10.0,10.0,9.0,4,2.18 +45421,72.0,8.0,8.0,8.0,8.0,8.0,8.0,5,1.92 +31419,,,,,,,,0, +22442,100.0,10.0,10.0,10.0,10.0,8.0,9.0,2,1.18 +50058,100.0,10.0,10.0,8.0,10.0,10.0,10.0,2,1.4 +70235,80.0,10.0,9.0,10.0,10.0,9.0,8.0,5,2.68 +31345,90.0,9.0,9.0,9.0,10.0,10.0,9.0,6,5.62 +31831,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +29868,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,4.0 +46811,,,,,,,,0, +50592,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.03 +71302,,,,,,,,0, +25361,,,,,,,,0, +5012,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,1.23 +31349,89.0,9.0,9.0,9.0,10.0,9.0,9.0,18,7.01 +1302,87.0,9.0,8.0,10.0,9.0,10.0,8.0,6,2.47 +74560,88.0,9.0,9.0,10.0,10.0,10.0,9.0,8,3.93 +14494,80.0,8.0,10.0,10.0,10.0,10.0,8.0,1,0.77 +38649,98.0,10.0,10.0,10.0,10.0,10.0,10.0,9,3.65 +25503,90.0,9.0,10.0,10.0,10.0,10.0,8.0,4,1.6 +25578,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.86 +13156,,,,,,,,1,0.94 +25295,,,,,,,,0, +22299,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +68932,98.0,10.0,10.0,10.0,10.0,10.0,10.0,25,10.0 +73148,88.0,9.0,9.0,9.0,10.0,9.0,10.0,17,7.61 +7590,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,1.43 +40797,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +41629,92.0,10.0,10.0,9.0,9.0,9.0,9.0,5,2.73 +45335,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,2.61 +71556,100.0,9.0,10.0,10.0,9.0,9.0,8.0,3,1.67 +61492,,,,,,,,0, +66921,84.0,9.0,9.0,10.0,9.0,8.0,8.0,16,6.58 +55942,73.0,9.0,7.0,9.0,9.0,10.0,9.0,4,1.58 +75830,97.0,10.0,10.0,10.0,10.0,9.0,10.0,14,5.53 +34119,,,,,,,,0, +51658,,,,,,,,0, +66457,90.0,9.0,10.0,9.0,9.0,10.0,9.0,25,9.87 +28253,95.0,10.0,10.0,10.0,10.0,9.0,10.0,15,6.62 +73185,96.0,10.0,10.0,9.0,10.0,10.0,9.0,8,3.75 +56307,73.0,7.0,8.0,8.0,8.0,9.0,8.0,3,1.64 +49465,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,3.75 +2060,87.0,10.0,9.0,10.0,10.0,10.0,9.0,3,1.3 +8154,100.0,10.0,9.0,10.0,10.0,9.0,10.0,3,2.09 +18372,98.0,10.0,9.0,10.0,10.0,10.0,10.0,12,5.29 +53106,,,,,,,,0, +75025,90.0,10.0,8.0,9.0,10.0,10.0,10.0,4,2.55 +46940,91.0,10.0,10.0,10.0,10.0,10.0,9.0,7,4.57 +44321,40.0,6.0,8.0,4.0,6.0,10.0,10.0,1,1.0 +36885,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,1.48 +1587,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,3.04 +72769,80.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.45 +15615,90.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.14 +9052,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.2 +51214,93.0,10.0,10.0,10.0,10.0,10.0,10.0,23,9.45 +62789,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,3.91 +33867,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +48805,97.0,10.0,10.0,10.0,10.0,10.0,10.0,6,2.9 +55396,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,2.31 +59408,93.0,10.0,10.0,10.0,10.0,10.0,9.0,8,3.87 +75279,,,,,,,,0, +1732,,,,,,,,1,0.43 +28274,,,,,,,,0, +29090,80.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.86 +39402,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +6130,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.0 +28038,97.0,10.0,10.0,10.0,10.0,10.0,10.0,27,11.41 +70955,99.0,10.0,9.0,10.0,10.0,10.0,10.0,22,10.15 +58280,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,5.45 +67259,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,4.14 +32421,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.05 +10428,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,3.39 +25014,90.0,9.0,9.0,10.0,10.0,9.0,10.0,2,0.98 +69901,,,,,,,,1,0.43 +72586,,,,,,,,0, +75953,,,,,,,,1,1.0 +76608,90.0,9.0,9.0,9.0,9.0,9.0,9.0,6,2.81 +11606,92.0,10.0,8.0,10.0,10.0,10.0,9.0,26,10.99 +66612,86.0,9.0,8.0,10.0,10.0,9.0,8.0,10,4.29 +73241,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.73 +11691,93.0,10.0,10.0,10.0,10.0,10.0,10.0,3,3.0 +19737,90.0,9.0,9.0,10.0,10.0,10.0,10.0,2,0.87 +3078,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,5.0 +17350,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,3.0 +65749,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.43 +59274,98.0,10.0,10.0,10.0,10.0,10.0,10.0,12,5.22 +60487,,,,,,,,0, +70183,,,,,,,,0, +39209,96.0,10.0,9.0,10.0,10.0,9.0,9.0,5,2.63 +33095,,,,,,,,0, +11377,67.0,5.0,7.0,7.0,7.0,8.0,7.0,3,1.5 +70205,80.0,8.0,6.0,8.0,10.0,10.0,10.0,1,0.53 +34336,,,,,,,,1,0.56 +26430,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.27 +61909,60.0,10.0,6.0,6.0,6.0,10.0,8.0,3,1.73 +68482,100.0,10.0,9.0,10.0,6.0,8.0,8.0,2,1.09 +53177,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +65435,80.0,7.0,8.0,9.0,10.0,8.0,9.0,3,1.64 +53715,80.0,10.0,10.0,9.0,6.0,10.0,8.0,2,1.18 +27442,,,,,,,,0, +19238,76.0,8.0,9.0,8.0,8.0,10.0,8.0,5,2.83 +41439,90.0,9.0,10.0,10.0,10.0,9.0,9.0,18,7.4 +21240,,,,,,,,0, +56975,,,,,,,,0, +61374,,,,,,,,0, +64595,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.43 +13943,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,0.87 +10725,100.0,8.0,10.0,6.0,8.0,10.0,10.0,1,0.88 +32869,98.0,10.0,10.0,10.0,10.0,10.0,10.0,11,4.65 +29859,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.25 +59763,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.86 +27924,,,,,,,,0, +28417,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,6.96 +68447,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.07 +51941,93.0,9.0,9.0,10.0,10.0,9.0,9.0,23,10.3 +17707,,,,,,,,0, +44277,,,,,,,,0, +26500,90.0,9.0,10.0,8.0,10.0,10.0,9.0,2,1.76 +66097,80.0,8.0,10.0,7.0,9.0,10.0,8.0,4,2.67 +55898,93.0,10.0,9.0,9.0,9.0,9.0,8.0,3,1.91 +57360,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.85 +54726,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,2.22 +74337,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,4.0 +22212,87.0,9.0,8.0,10.0,10.0,10.0,9.0,3,1.84 +23251,,,,,,,,0, +24620,,,,,,,,0, +23145,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,5.0 +22143,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.52 +43620,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.11 +47981,100.0,10.0,10.0,10.0,10.0,10.0,10.0,10,4.92 +43016,100.0,10.0,10.0,10.0,10.0,6.0,8.0,1,1.0 +22844,93.0,10.0,9.0,9.0,9.0,9.0,9.0,3,1.3 +5531,,,,,,,,0, +53218,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.45 +61811,,,,,,,,0, +48594,40.0,2.0,10.0,8.0,2.0,10.0,8.0,1,0.81 +62155,90.0,10.0,8.0,10.0,10.0,10.0,8.0,2,0.95 +16277,,,,,,,,0, +69225,100.0,10.0,10.0,10.0,10.0,9.0,8.0,5,2.73 +28182,80.0,8.0,10.0,10.0,10.0,10.0,6.0,1,0.48 +6943,,,,,,,,0, +12819,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,2.61 +41289,80.0,10.0,8.0,10.0,10.0,8.0,8.0,2,2.0 +22301,94.0,9.0,9.0,10.0,10.0,9.0,9.0,7,3.39 +45919,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.6 +15461,,,,,,,,0, +12062,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,2.05 +66270,,,,,,,,0, +65933,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +59872,90.0,9.0,10.0,8.0,7.0,9.0,9.0,2,1.4 +13346,,,,,,,,0, +54211,90.0,9.0,9.0,10.0,9.0,10.0,10.0,7,3.23 +26115,,,,,,,,0, +11776,96.0,10.0,10.0,10.0,10.0,10.0,9.0,9,4.29 +41320,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,1.3 +33376,80.0,9.0,7.0,9.0,9.0,9.0,8.0,11,5.16 +17491,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.43 +16090,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.47 +43427,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.53 +54525,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +65103,96.0,9.0,10.0,10.0,10.0,8.0,9.0,10,4.76 +65368,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,5.74 +30560,90.0,9.0,9.0,7.0,10.0,9.0,8.0,6,2.86 +20430,99.0,10.0,10.0,10.0,10.0,10.0,10.0,14,6.67 +69676,,,,,,,,0, +69032,,,,,,,,0, +55862,,,,,,,,0, +54891,,,,,,,,0, +41604,,,,,,,,0, +24491,,,,,,,,0, +66155,80.0,7.0,8.0,8.0,8.0,9.0,8.0,3,1.76 +2959,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +19337,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.86 +6136,,,,,,,,0, +48338,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.6 +23660,,,,,,,,0, +73435,,,,,,,,0, +9188,,,,,,,,0, +27070,40.0,2.0,2.0,8.0,8.0,6.0,2.0,1,0.79 +50890,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,1.36 +5362,80.0,10.0,10.0,4.0,10.0,10.0,10.0,1,1.0 +23258,,,,,,,,0, +74550,60.0,6.0,6.0,8.0,6.0,6.0,6.0,1,0.75 +66071,,,,,,,,0, +7336,60.0,8.0,8.0,10.0,10.0,10.0,6.0,1,0.91 +29365,90.0,9.0,9.0,9.0,9.0,9.0,9.0,6,3.83 +8063,,,,,,,,0, +37713,,,,,,,,0, +71083,91.0,10.0,9.0,10.0,10.0,10.0,9.0,9,4.29 +893,,,,,,,,0, +22089,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.36 +47987,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.8 +27424,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,1.79 +24175,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.91 +19903,,,,,,,,0, +60147,97.0,10.0,9.0,10.0,10.0,10.0,9.0,7,5.83 +62993,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.6 +44503,,,,,,,,0, +47178,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.55 +42212,80.0,7.0,10.0,9.0,9.0,9.0,8.0,2,1.76 +52231,84.0,9.0,9.0,9.0,8.0,10.0,8.0,5,2.42 +73524,86.0,9.0,9.0,10.0,9.0,10.0,9.0,7,3.82 +45693,,,,,,,,0, +9762,,,,,,,,0, +1686,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,2.73 +45902,,,,,,,,0, +32436,70.0,9.0,5.0,10.0,10.0,10.0,8.0,4,3.53 +23376,,,,,,,,0, +66123,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,2.43 +64353,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,2.68 +49875,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.61 +74501,88.0,10.0,9.0,10.0,10.0,10.0,9.0,5,2.31 +23538,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.61 +38891,100.0,10.0,10.0,9.0,10.0,10.0,10.0,3,1.7 +40059,90.0,10.0,10.0,9.0,9.0,10.0,10.0,4,2.93 +37328,,,,,,,,1,0.55 +9071,40.0,10.0,4.0,8.0,8.0,6.0,4.0,1,1.0 +56739,80.0,9.0,8.0,9.0,8.0,8.0,8.0,6,2.95 +76230,95.0,10.0,9.0,9.0,10.0,10.0,9.0,4,2.22 +36044,,,,,,,,0, +62163,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,0.61 +38259,93.0,9.0,9.0,10.0,9.0,10.0,9.0,3,2.05 +22362,,,,,,,,0, +60364,91.0,10.0,10.0,10.0,10.0,10.0,9.0,9,4.91 +28306,,,,,,,,0, +57618,,,,,,,,0, +3023,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.57 +65241,,,,,,,,0, +24235,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,2.34 +23051,93.0,10.0,10.0,10.0,9.0,10.0,9.0,3,1.91 +21307,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.55 +57478,,,,,,,,0, +19687,,,,,,,,0, +73005,91.0,10.0,9.0,9.0,10.0,10.0,9.0,7,3.23 +47137,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +72233,,,,,,,,0, +27584,73.0,9.0,7.0,9.0,9.0,7.0,8.0,3,3.0 +28186,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.73 +3400,,,,,,,,0, +76567,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +61158,,,,,,,,0, +5009,95.0,10.0,10.0,10.0,9.0,10.0,10.0,8,4.21 +34471,,,,,,,,0, +49275,,,,,,,,0, +66074,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.83 +52913,96.0,10.0,9.0,10.0,10.0,10.0,10.0,10,6.82 +10474,,,,,,,,0, +17116,90.0,10.0,10.0,10.0,10.0,8.0,9.0,2,1.11 +26873,,,,,,,,0, +58144,100.0,10.0,8.0,10.0,10.0,8.0,9.0,2,1.09 +10385,78.0,8.0,7.0,9.0,10.0,9.0,8.0,8,6.32 +55494,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.2 +16054,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,2.42 +54514,,,,,,,,0, +10100,,,,,,,,0, +22474,97.0,10.0,10.0,10.0,10.0,10.0,9.0,6,3.4 +6208,100.0,10.0,10.0,10.0,10.0,9.0,10.0,7,4.2 +10502,84.0,10.0,9.0,10.0,10.0,10.0,9.0,5,3.75 +42468,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,2.55 +15103,,,,,,,,0, +38688,,,,,,,,0, +35025,100.0,8.0,10.0,10.0,10.0,10.0,10.0,1,0.79 +74706,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.41 +35600,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +48629,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +64170,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,1.55 +35749,,,,,,,,1,1.0 +495,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.22 +19303,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.5 +19421,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.71 +14078,80.0,9.0,10.0,9.0,10.0,9.0,8.0,2,1.54 +20631,100.0,10.0,10.0,10.0,10.0,10.0,8.0,2,1.2 +37306,70.0,8.0,8.0,8.0,9.0,8.0,7.0,2,2.0 +54069,,,,,,,,0, +3658,40.0,2.0,6.0,10.0,6.0,10.0,4.0,1,0.63 +62296,94.0,10.0,10.0,10.0,10.0,9.0,9.0,19,10.56 +51168,,,,,,,,0, +30733,,,,,,,,0, +3664,,,,,,,,1, +18836,,,,,,,,0, +75831,,,,,,,,0, +32847,,,,,,,,0, +38081,,,,,,,,0, +44664,93.0,10.0,9.0,10.0,10.0,10.0,10.0,3,3.0 +60040,100.0,10.0,10.0,10.0,9.0,9.0,10.0,2,1.05 +2897,,,,,,,,0, +30407,100.0,10.0,10.0,10.0,10.0,10.0,10.0,19,8.91 +68274,95.0,10.0,10.0,9.0,10.0,9.0,9.0,4,2.55 +6345,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.86 +23773,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,4.44 +31698,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.48 +45050,77.0,9.0,9.0,10.0,10.0,8.0,8.0,8,4.44 +29860,96.0,10.0,9.0,10.0,10.0,10.0,10.0,5,3.13 +19445,94.0,10.0,9.0,9.0,10.0,10.0,10.0,7,7.0 +18510,,,,,,,,0, +28945,,,,,,,,0, +38298,,,,,,,,0, +554,,,,,,,,0, +15800,,,,,,,,0, +62932,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.63 +22256,100.0,10.0,10.0,10.0,10.0,7.0,10.0,2,1.05 +4416,,,,,,,,1,0.6 +43839,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.67 +4429,80.0,10.0,10.0,10.0,2.0,10.0,8.0,2,2.0 +3630,87.0,9.0,9.0,9.0,9.0,10.0,9.0,3,1.91 +38960,,,,,,,,0, +71122,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.18 +29368,,,,,,,,0, +76397,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.71 +8215,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +64287,100.0,10.0,9.0,10.0,10.0,10.0,9.0,4,2.26 +14536,97.0,10.0,10.0,10.0,10.0,9.0,9.0,6,2.9 +68659,,,,,,,,0, +9781,85.0,9.0,9.0,9.0,10.0,10.0,8.0,4,2.35 +12893,94.0,10.0,9.0,10.0,10.0,9.0,10.0,7,3.39 +40663,,,,,,,,0, +51607,,,,,,,,1, +12350,80.0,10.0,8.0,10.0,10.0,8.0,8.0,1,1.0 +27231,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,1.0 +6619,,,,,,,,0, +36064,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,1.33 +7843,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.22 +10062,,,,,,,,0, +37935,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,2.0 +75234,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +55392,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.59 +40439,100.0,9.0,10.0,9.0,10.0,10.0,9.0,7,4.2 +62719,93.0,10.0,10.0,10.0,10.0,10.0,10.0,9,6.92 +27100,80.0,8.0,7.0,8.0,9.0,9.0,8.0,6,3.6 +55031,67.0,9.0,7.0,9.0,8.0,9.0,8.0,3,1.64 +63314,60.0,7.0,4.0,7.0,8.0,7.0,6.0,5,3.66 +67813,,,,,,,,0, +5549,100.0,8.0,10.0,6.0,10.0,8.0,10.0,1,1.0 +41389,80.0,8.0,10.0,10.0,10.0,10.0,8.0,2,2.0 +24941,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.09 +17049,,,,,,,,1, +37496,100.0,10.0,10.0,10.0,10.0,10.0,10.0,9,5.0 +59568,,,,,,,,0, +65846,60.0,8.0,6.0,10.0,6.0,8.0,10.0,1,0.55 +38055,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,3.33 +60071,,,,,,,,0, +19443,100.0,10.0,10.0,10.0,10.0,9.0,9.0,2,1.33 +60175,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,2.0 +19047,100.0,10.0,10.0,10.0,9.0,9.0,10.0,5,3.66 +38987,100.0,10.0,10.0,10.0,10.0,9.0,9.0,6,6.0 +26321,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +71893,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +13863,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.88 +11020,100.0,9.0,10.0,9.0,10.0,10.0,10.0,2,2.0 +48494,93.0,10.0,10.0,9.0,10.0,10.0,9.0,3,3.0 +16614,,,,,,,,0, +11758,,,,,,,,0, +73519,,,,,,,,0, +69368,100.0,10.0,9.0,10.0,10.0,10.0,10.0,6,6.0 +9920,,,,,,,,0, +6365,,,,,,,,0, +61688,,,,,,,,0, +38072,,,,,,,,1,0.58 +43663,82.0,9.0,8.0,9.0,10.0,9.0,8.0,11,6.0 +76962,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.45 +52936,,,,,,,,0, +58004,,,,,,,,0, +10312,98.0,10.0,10.0,10.0,10.0,10.0,10.0,8,5.11 +13903,92.0,10.0,9.0,10.0,10.0,8.0,10.0,5,5.0 +5942,,,,,,,,0, +60641,83.0,9.0,10.0,10.0,9.0,10.0,9.0,8,4.21 +62908,,,,,,,,0, +28564,90.0,9.0,10.0,9.0,9.0,10.0,9.0,8,5.0 +58975,,,,,,,,0, +52852,91.0,9.0,9.0,10.0,10.0,10.0,9.0,7,3.89 +22285,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +49554,,,,,,,,0, +36052,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,2.0 +30605,96.0,10.0,10.0,10.0,10.0,10.0,9.0,10,5.45 +24285,100.0,10.0,10.0,10.0,10.0,10.0,8.0,2,2.0 +31163,93.0,10.0,10.0,10.0,10.0,10.0,9.0,3,1.73 +32174,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +49897,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,4.0 +39261,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,4.38 +43979,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +34083,96.0,10.0,9.0,10.0,10.0,10.0,10.0,5,2.73 +45936,97.0,10.0,10.0,9.0,10.0,10.0,10.0,6,4.5 +3081,40.0,4.0,10.0,6.0,8.0,10.0,4.0,1,1.0 +52628,,,,,,,,0, +9088,80.0,10.0,10.0,10.0,10.0,10.0,6.0,1,0.73 +40763,97.0,10.0,10.0,10.0,10.0,9.0,10.0,6,3.75 +28483,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +34278,,,,,,,,0, +64235,20.0,2.0,2.0,4.0,2.0,6.0,2.0,1,0.91 +65723,,,,,,,,0, +45455,,,,,,,,0, +65508,98.0,10.0,10.0,10.0,10.0,10.0,10.0,17,9.27 +22267,93.0,9.0,9.0,9.0,10.0,9.0,9.0,8,4.62 +6708,,,,,,,,1,0.67 +60061,,,,,,,,0, +37326,,,,,,,,0, +21248,,,,,,,,0, +34426,95.0,10.0,10.0,10.0,10.0,9.0,10.0,8,5.22 +14547,90.0,10.0,10.0,10.0,10.0,10.0,10.0,4,3.08 +2443,,,,,,,,1, +63352,87.0,9.0,8.0,9.0,10.0,8.0,7.0,3,1.61 +2594,100.0,10.0,10.0,10.0,10.0,9.0,10.0,2,1.46 +23331,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.75 +20872,90.0,8.0,10.0,6.0,8.0,8.0,10.0,4,3.43 +38823,,,,,,,,0, +25518,,,,,,,,0, +25452,100.0,10.0,10.0,8.0,9.0,9.0,9.0,3,3.0 +63259,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.11 +26160,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,3.0 +36289,100.0,10.0,10.0,10.0,10.0,9.0,10.0,5,4.29 +42769,,,,,,,,0, +33486,76.0,9.0,8.0,9.0,9.0,9.0,9.0,10,5.88 +41809,80.0,6.0,8.0,4.0,10.0,10.0,10.0,1,1.0 +42327,,,,,,,,0, +24230,,,,,,,,0, +76372,,,,,,,,0, +28338,50.0,5.0,5.0,5.0,5.0,6.0,6.0,2,1.46 +32222,,,,,,,,1,0.83 +63188,,,,,,,,0, +11627,,,,,,,,0, +29003,,,,,,,,0, +8090,80.0,2.0,6.0,10.0,10.0,10.0,10.0,1,1.0 +11963,,,,,,,,0, +15911,,,,,,,,0, +17821,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.43 +40993,100.0,9.0,10.0,10.0,10.0,10.0,10.0,3,2.5 +34961,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,0.75 +49820,,,,,,,,0, +27546,85.0,10.0,10.0,10.0,10.0,10.0,9.0,4,3.0 +62128,,,,,,,,0, +24898,,,,,,,,0, +44091,94.0,10.0,9.0,10.0,10.0,10.0,9.0,10,5.56 +26813,80.0,10.0,4.0,6.0,10.0,8.0,6.0,1,1.0 +16679,93.0,10.0,10.0,10.0,10.0,8.0,9.0,3,2.31 +12297,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +38107,95.0,9.0,9.0,9.0,10.0,10.0,10.0,4,2.73 +33342,87.0,9.0,8.0,9.0,10.0,10.0,8.0,3,2.43 +53040,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.88 +66137,,,,,,,,1,1.0 +153,60.0,10.0,10.0,10.0,10.0,8.0,6.0,1,1.0 +63877,,,,,,,,0, +24194,,,,,,,,0, +25211,70.0,6.0,8.0,8.0,7.0,8.0,7.0,3,1.76 +76633,,,,,,,,0, +54644,,,,,,,,0, +19721,84.0,10.0,8.0,10.0,10.0,9.0,8.0,5,3.66 +41562,,,,,,,,0, +58058,,,,,,,,0, +70202,77.0,8.0,9.0,10.0,10.0,9.0,8.0,6,3.33 +47168,,,,,,,,2,1.71 +54777,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.55 +15410,80.0,8.0,10.0,10.0,9.0,10.0,7.0,3,1.88 +52546,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +34455,93.0,9.0,9.0,9.0,10.0,9.0,9.0,6,3.83 +33380,,,,,,,,0, +203,,,,,,,,0, +20446,100.0,10.0,10.0,10.0,10.0,10.0,10.0,14,9.13 +74993,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,1.7 +11698,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,4.05 +35510,,,,,,,,1,0.67 +54635,100.0,10.0,10.0,10.0,8.0,10.0,10.0,1,1.0 +76090,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +44988,100.0,10.0,10.0,10.0,10.0,10.0,10.0,13,8.13 +41119,,,,,,,,0, +2207,,,,,,,,0, +32119,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.93 +51116,,,,,,,,0, +31215,90.0,10.0,6.0,10.0,9.0,9.0,9.0,2,2.0 +7078,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,2.05 +33321,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.22 +27688,84.0,9.0,10.0,10.0,9.0,9.0,9.0,11,6.23 +55401,93.0,9.0,9.0,10.0,10.0,9.0,9.0,3,2.57 +73674,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,6.0 +64309,,,,,,,,0, +57769,100.0,10.0,10.0,10.0,10.0,9.0,10.0,6,3.46 +34193,,,,,,,,0, +14644,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,2.05 +33748,100.0,8.0,8.0,8.0,10.0,10.0,8.0,1,0.75 +64067,95.0,10.0,10.0,10.0,10.0,10.0,10.0,8,5.85 +69030,80.0,10.0,10.0,10.0,10.0,10.0,8.0,1,0.79 +44397,,,,,,,,0, +23344,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +34721,93.0,10.0,10.0,10.0,10.0,10.0,10.0,3,2.43 +36412,100.0,10.0,10.0,10.0,10.0,9.0,10.0,4,2.86 +22940,80.0,10.0,9.0,8.0,9.0,10.0,9.0,5,2.94 +8755,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,5.0 +32823,,,,,,,,0, +56613,,,,,,,,0, +35624,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,0.83 +63745,,,,,,,,2,1.25 +37555,80.0,9.0,7.0,9.0,10.0,10.0,7.0,3,1.91 +29986,,,,,,,,0, +38992,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.88 +17998,75.0,9.0,7.0,9.0,9.0,10.0,8.0,8,6.32 +2391,80.0,10.0,8.0,10.0,10.0,10.0,8.0,2,2.0 +75013,93.0,10.0,9.0,10.0,10.0,10.0,9.0,3,2.65 +24330,92.0,9.0,10.0,10.0,10.0,10.0,9.0,5,3.66 +12824,80.0,10.0,6.0,10.0,10.0,8.0,6.0,1,0.88 +3673,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.64 +60947,89.0,9.0,10.0,8.0,9.0,10.0,9.0,7,6.36 +22020,100.0,10.0,10.0,9.0,10.0,10.0,10.0,9,5.87 +6036,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,3.83 +73003,100.0,10.0,6.0,10.0,10.0,8.0,10.0,1,1.0 +54029,100.0,8.0,8.0,10.0,10.0,10.0,10.0,1,0.83 +51840,,,,,,,,0, +72913,,,,,,,,0, +29876,98.0,10.0,10.0,10.0,10.0,9.0,10.0,8,5.85 +45469,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +70767,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,2.55 +68618,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,3.64 +26266,93.0,10.0,10.0,9.0,6.0,9.0,9.0,3,3.0 +8088,95.0,9.0,9.0,9.0,9.0,10.0,9.0,4,3.08 +35418,100.0,10.0,8.0,10.0,8.0,10.0,10.0,1,1.0 +10169,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.86 +76257,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.65 +35422,,,,,,,,0, +2480,80.0,10.0,10.0,6.0,8.0,10.0,10.0,1,0.73 +32527,,,,,,,,0, +7184,,,,,,,,0, +9884,,,,,,,,0, +73302,100.0,10.0,9.0,10.0,10.0,10.0,10.0,6,4.86 +22257,92.0,10.0,9.0,10.0,10.0,9.0,10.0,12,7.83 +52781,98.0,10.0,10.0,10.0,10.0,9.0,10.0,9,5.63 +47681,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,1.28 +2339,67.0,10.0,9.0,6.0,8.0,9.0,9.0,9,7.94 +69760,97.0,10.0,10.0,10.0,10.0,10.0,10.0,7,4.77 +31121,,,,,,,,0, +27890,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,3.0 +72590,,,,,,,,0, +59311,,,,,,,,0, +34560,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +18861,91.0,10.0,9.0,9.0,10.0,10.0,9.0,9,7.94 +73438,,,,,,,,0, +25092,,,,,,,,0, +53682,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +72216,,,,,,,,0, +61558,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,1.0 +3862,,,,,,,,0, +50999,94.0,10.0,9.0,10.0,10.0,10.0,9.0,7,7.0 +12699,95.0,10.0,10.0,10.0,10.0,10.0,9.0,4,3.53 +71049,,,,,,,,0, +58229,100.0,9.0,10.0,9.0,10.0,10.0,10.0,3,2.37 +62839,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +10136,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +41858,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,2.0 +7302,90.0,10.0,10.0,9.0,10.0,10.0,10.0,4,3.33 +10406,,,,,,,,1,0.68 +23827,,,,,,,,1,0.7 +74580,,,,,,,,0, +10476,100.0,10.0,10.0,10.0,10.0,10.0,9.0,4,2.79 +67717,60.0,6.0,8.0,8.0,8.0,10.0,6.0,3,3.0 +16178,40.0,4.0,4.0,6.0,4.0,10.0,6.0,1,1.0 +57487,88.0,10.0,8.0,10.0,10.0,10.0,10.0,5,3.49 +25732,,,,,,,,0, +18289,70.0,8.0,6.0,9.0,10.0,10.0,7.0,2,2.0 +35547,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,3.53 +37052,,,,,,,,0, +62037,95.0,10.0,9.0,10.0,10.0,10.0,10.0,4,4.0 +18225,98.0,10.0,9.0,9.0,9.0,10.0,10.0,8,6.0 +45740,93.0,9.0,10.0,10.0,10.0,10.0,10.0,3,2.2 +57822,20.0,2.0,2.0,2.0,2.0,2.0,2.0,1,1.0 +50386,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +14096,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,5.85 +56233,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,2.0 +15768,,,,,,,,0, +31074,,,,,,,,0, +2819,,,,,,,,0, +37241,,,,,,,,0, +63426,,,,,,,,0, +46939,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +4348,,,,,,,,0, +38658,,,,,,,,0, +40890,,,,,,,,1, +41698,90.0,9.0,8.0,10.0,10.0,8.0,10.0,2,1.54 +29779,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,3.0 +32796,,,,,,,,0, +33005,90.0,10.0,8.0,10.0,10.0,10.0,10.0,2,1.82 +4886,,,,,,,,0, +67436,,,,,,,,0, +69730,93.0,10.0,9.0,10.0,9.0,10.0,10.0,12,7.83 +57495,,,,,,,,0, +67125,87.0,9.0,10.0,8.0,9.0,9.0,8.0,3,3.0 +37645,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +47299,,,,,,,,0, +13281,100.0,10.0,9.0,10.0,10.0,10.0,10.0,4,3.24 +33575,67.0,7.0,6.0,7.0,7.0,8.0,7.0,3,2.25 +23588,,,,,,,,0, +1613,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.46 +62462,,,,,,,,0, +15412,86.0,10.0,8.0,9.0,10.0,10.0,9.0,7,5.83 +9822,75.0,8.0,8.0,9.0,10.0,10.0,7.0,4,2.73 +14041,95.0,10.0,10.0,10.0,10.0,9.0,10.0,4,2.73 +16439,,,,,,,,0, +19152,,,,,,,,1, +19385,100.0,10.0,10.0,10.0,10.0,10.0,10.0,5,4.05 +22371,,,,,,,,1, +11908,,,,,,,,0, +60423,,,,,,,,0, +64406,80.0,10.0,9.0,8.0,7.0,9.0,9.0,2,2.0 +71376,,,,,,,,0, +25966,,,,,,,,0, +27539,,,,,,,,1,0.65 +9660,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +41447,,,,,,,,0, +1830,68.0,7.0,6.0,8.0,9.0,10.0,7.0,5,3.85 +54454,,,,,,,,0, +75494,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.43 +55734,,,,,,,,0, +76281,100.0,10.0,8.0,10.0,10.0,8.0,10.0,1,1.0 +28812,,,,,,,,0, +40401,,,,,,,,0, +30894,96.0,10.0,9.0,10.0,10.0,9.0,10.0,5,4.05 +49255,90.0,9.0,10.0,10.0,10.0,9.0,9.0,4,2.93 +7257,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +70168,,,,,,,,0, +3310,90.0,9.0,9.0,10.0,10.0,9.0,9.0,6,4.86 +36512,85.0,10.0,9.0,10.0,10.0,8.0,9.0,4,3.0 +55803,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +43377,90.0,9.0,9.0,10.0,10.0,10.0,10.0,2,2.0 +68499,,,,,,,,1,1.0 +38556,,,,,,,,1,1.0 +6544,60.0,10.0,8.0,10.0,10.0,10.0,6.0,1,1.0 +18945,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,3.0 +74393,,,,,,,,0, +64699,,,,,,,,0, +5377,,,,,,,,0, +17529,100.0,10.0,10.0,10.0,10.0,6.0,8.0,1,1.0 +28544,,,,,,,,0, +73055,87.0,9.0,7.0,10.0,10.0,10.0,9.0,3,3.0 +45780,,,,,,,,0, +60272,,,,,,,,0, +4200,40.0,4.0,2.0,10.0,2.0,10.0,4.0,1,1.0 +15009,100.0,9.0,10.0,10.0,9.0,9.0,8.0,2,2.0 +31793,80.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +30082,,,,,,,,0, +13388,90.0,7.0,8.0,9.0,9.0,9.0,8.0,2,2.0 +48475,80.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +74102,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +12221,,,,,,,,0, +19383,,,,,,,,0, +15781,100.0,8.0,8.0,6.0,10.0,10.0,10.0,1,1.0 +48193,,,,,,,,0, +22492,85.0,9.0,10.0,10.0,10.0,9.0,9.0,4,4.0 +43924,,,,,,,,0, +50414,,,,,,,,0, +62708,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,4.0 +50303,,,,,,,,0, +38501,40.0,8.0,6.0,10.0,10.0,10.0,6.0,1,1.0 +58839,,,,,,,,0, +58559,,,,,,,,0, +26534,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +57542,,,,,,,,0, +54718,,,,,,,,0, +61116,,,,,,,,0, +59670,100.0,10.0,8.0,10.0,10.0,10.0,10.0,1,0.86 +70462,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +28089,,,,,,,,0, +75506,100.0,10.0,10.0,10.0,10.0,10.0,10.0,8,7.06 +67606,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +3062,,,,,,,,0, +3440,,,,,,,,0, +52344,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +40710,100.0,10.0,10.0,8.0,9.0,10.0,10.0,6,4.0 +16516,,,,,,,,0, +35577,60.0,4.0,6.0,8.0,4.0,8.0,6.0,1,1.0 +58390,100.0,10.0,10.0,10.0,10.0,10.0,9.0,8,5.58 +66105,,,,,,,,0, +35765,,,,,,,,0, +39585,,,,,,,,0, +7652,80.0,10.0,8.0,10.0,10.0,10.0,8.0,1,1.0 +47757,,,,,,,,0, +28456,20.0,4.0,2.0,4.0,2.0,8.0,2.0,1,1.0 +17147,93.0,10.0,9.0,9.0,10.0,9.0,9.0,9,6.43 +34343,,,,,,,,0, +48554,,,,,,,,0, +40881,,,,,,,,0, +18240,,,,,,,,0, +35759,90.0,10.0,10.0,10.0,8.0,9.0,10.0,2,2.0 +13392,80.0,10.0,6.0,10.0,8.0,8.0,8.0,1,1.0 +43934,,,,,,,,0, +59107,,,,,,,,0, +26915,80.0,2.0,2.0,2.0,2.0,2.0,2.0,1,0.81 +52796,,,,,,,,0, +64096,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +28022,93.0,9.0,10.0,9.0,10.0,9.0,9.0,3,3.0 +24082,,,,,,,,0, +43252,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +17924,,,,,,,,0, +8975,,,,,,,,0, +52293,,,,,,,,0, +28049,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +28230,80.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.5 +12835,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,1.54 +68713,,,,,,,,0, +11900,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +62627,,,,,,,,0, +51794,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.62 +31653,,,,,,,,0, +11241,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +21834,,,,,,,,0, +55993,,,,,,,,0, +69733,,,,,,,,0, +41074,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +22600,60.0,10.0,10.0,10.0,7.0,9.0,7.0,2,1.71 +23183,60.0,10.0,10.0,10.0,10.0,10.0,6.0,1,1.0 +42191,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +35113,,,,,,,,0, +23875,,,,,,,,0, +57732,,,,,,,,0, +23043,92.0,10.0,9.0,10.0,10.0,10.0,8.0,5,4.05 +6257,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.88 +35590,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +45070,60.0,6.0,6.0,10.0,10.0,10.0,10.0,1,1.0 +9428,90.0,9.0,8.0,9.0,10.0,9.0,9.0,2,2.0 +60894,93.0,10.0,10.0,10.0,10.0,10.0,9.0,6,4.39 +23712,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +52958,100.0,10.0,10.0,10.0,10.0,6.0,10.0,1,1.0 +32843,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.73 +35049,,,,,,,,0, +44379,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,1.54 +865,60.0,4.0,4.0,6.0,8.0,8.0,4.0,1,1.0 +45534,,,,,,,,0, +53306,98.0,10.0,10.0,10.0,10.0,9.0,9.0,10,8.82 +36891,93.0,10.0,9.0,9.0,9.0,10.0,9.0,6,6.0 +39903,100.0,10.0,10.0,7.0,10.0,8.0,9.0,2,2.0 +13927,,,,,,,,0, +36126,92.0,10.0,10.0,10.0,10.0,10.0,9.0,5,5.0 +54228,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,2.65 +8667,,,,,,,,0, +1678,,,,,,,,0, +29013,,,,,,,,0, +44765,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.82 +7731,,,,,,,,0, +32985,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +12473,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.58 +21508,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.91 +67458,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,2.0 +40251,,,,,,,,0, +41034,90.0,9.0,9.0,9.0,9.0,9.0,9.0,2,1.67 +23245,100.0,10.0,9.0,10.0,9.0,9.0,10.0,2,2.0 +29843,,,,,,,,0, +59623,80.0,10.0,8.0,10.0,10.0,10.0,9.0,2,2.0 +19798,,,,,,,,0, +7472,90.0,9.0,9.0,9.0,10.0,8.0,9.0,2,2.0 +54637,80.0,9.0,7.0,9.0,10.0,10.0,9.0,2,2.0 +73901,100.0,9.0,10.0,9.0,10.0,9.0,9.0,3,2.65 +70176,,,,,,,,0, +30030,73.0,9.0,8.0,10.0,10.0,10.0,9.0,3,2.37 +58461,,,,,,,,0, +20761,,,,,,,,0, +71245,,,,,,,,0, +25591,,,,,,,,0, +65973,,,,,,,,0, +45124,,,,,,,,0, +58367,,,,,,,,0, +75489,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +16966,100.0,10.0,9.0,10.0,10.0,10.0,10.0,3,3.0 +30932,,,,,,,,0, +32878,,,,,,,,0, +7743,,,,,,,,0, +15598,40.0,4.0,2.0,2.0,4.0,4.0,4.0,1,1.0 +62757,,,,,,,,0, +12977,,,,,,,,1,1.0 +22291,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +34308,93.0,9.0,9.0,9.0,10.0,10.0,9.0,3,3.0 +69086,60.0,7.0,6.0,8.0,8.0,9.0,7.0,2,2.0 +51796,,,,,,,,0, +8640,,,,,,,,0, +47012,60.0,8.0,8.0,10.0,10.0,10.0,6.0,1,1.0 +58128,,,,,,,,0, +31606,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +32777,,,,,,,,0, +4434,,,,,,,,0, +68748,80.0,6.0,6.0,10.0,10.0,10.0,8.0,1,1.0 +41977,90.0,10.0,8.0,10.0,10.0,9.0,10.0,2,2.0 +2532,,,,,,,,0, +15150,90.0,10.0,10.0,10.0,10.0,10.0,9.0,3,3.0 +8,,,,,,,,0, +1582,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +68988,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +74809,20.0,2.0,2.0,2.0,2.0,2.0,2.0,1,0.79 +26315,95.0,10.0,10.0,10.0,10.0,10.0,10.0,4,4.0 +38056,100.0,9.0,9.0,10.0,9.0,9.0,9.0,3,3.0 +44345,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +19890,,,,,,,,0, +25925,,,,,,,,0, +61766,80.0,10.0,10.0,10.0,10.0,4.0,8.0,1,1.0 +46983,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +64848,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +69545,,,,,,,,0, +31613,,,,,,,,0, +47526,,,,,,,,0, +25422,,,,,,,,0, +76359,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.88 +16627,,,,,,,,0, +42376,,,,,,,,0, +45317,,,,,,,,0, +68021,,,,,,,,0, +73416,,,,,,,,0, +49286,,,,,,,,0, +23225,,,,,,,,0, +32604,,,,,,,,0, +13618,,,,,,,,0, +16689,,,,,,,,0, +40516,,,,,,,,0, +1513,80.0,6.0,8.0,10.0,8.0,10.0,10.0,1,1.0 +74319,40.0,6.0,2.0,8.0,6.0,10.0,4.0,1,1.0 +35146,,,,,,,,1,1.0 +10361,,,,,,,,0, +71604,,,,,,,,0, +66296,,,,,,,,0, +56336,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +8880,,,,,,,,0, +44535,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +21173,,,,,,,,0, +50741,,,,,,,,0, +64270,,,,,,,,0, +9823,,,,,,,,0, +16484,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.62 +69620,,,,,,,,0, +44215,,,,,,,,0, +38096,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,1.87 +14969,,,,,,,,0, +43244,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,2.37 +1571,97.0,10.0,9.0,10.0,10.0,9.0,10.0,6,5.29 +47082,,,,,,,,0, +50889,100.0,8.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +6893,,,,,,,,0, +63749,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +63530,100.0,10.0,10.0,8.0,10.0,10.0,10.0,3,3.0 +40296,,,,,,,,0, +19623,,,,,,,,1,1.0 +57057,,,,,,,,1,0.79 +10457,,,,,,,,0, +10901,,,,,,,,0, +29923,,,,,,,,0, +50396,,,,,,,,0, +9060,,,,,,,,0, +35217,40.0,4.0,2.0,6.0,2.0,8.0,6.0,1,1.0 +23626,,,,,,,,0, +24985,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,1.0 +11206,89.0,9.0,7.0,10.0,10.0,9.0,9.0,7,5.83 +42285,70.0,7.0,6.0,8.0,7.0,9.0,7.0,2,1.76 +56447,,,,,,,,0, +15976,,,,,,,,0, +4267,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +38996,,,,,,,,0, +61139,,,,,,,,0, +64466,,,,,,,,0, +52228,94.0,9.0,8.0,10.0,10.0,8.0,9.0,7,7.0 +35646,90.0,9.0,8.0,10.0,10.0,9.0,9.0,6,5.14 +42239,,,,,,,,0, +2609,100.0,10.0,10.0,9.0,10.0,10.0,9.0,3,3.0 +52584,,,,,,,,0, +33944,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,0.86 +63796,100.0,10.0,10.0,10.0,10.0,10.0,9.0,6,5.14 +38918,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +60245,90.0,9.0,8.0,10.0,10.0,9.0,9.0,2,2.0 +75287,,,,,,,,0, +9890,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +9871,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,1.0 +53297,,,,,,,,0, +31610,,,,,,,,0, +49307,,,,,,,,0, +76553,100.0,10.0,10.0,10.0,10.0,10.0,9.0,2,2.0 +72400,,,,,,,,0, +56742,,,,,,,,0, +62387,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +53425,,,,,,,,0, +44602,,,,,,,,0, +23758,,,,,,,,0, +24166,,,,,,,,0, +36221,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,7.0 +1461,60.0,8.0,8.0,10.0,10.0,10.0,10.0,1,1.0 +57689,,,,,,,,0, +16492,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +19692,90.0,8.0,9.0,9.0,9.0,10.0,10.0,2,2.0 +70017,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +74340,,,,,,,,0, +10095,100.0,10.0,10.0,10.0,10.0,9.0,9.0,4,4.0 +47247,,,,,,,,0, +11581,100.0,10.0,10.0,9.0,10.0,9.0,10.0,4,3.64 +42709,,,,,,,,0, +9868,,,,,,,,0, +6927,,,,,,,,0, +64516,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +26189,100.0,10.0,10.0,10.0,10.0,10.0,8.0,2,1.71 +44805,,,,,,,,1,1.0 +44978,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +38602,,,,,,,,0, +20035,,,,,,,,0, +12352,,,,,,,,0, +35160,,,,,,,,0, +14749,90.0,8.0,8.0,9.0,10.0,8.0,8.0,2,2.0 +32229,,,,,,,,0, +1189,,,,,,,,0, +60258,,,,,,,,0, +43441,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +29541,,,,,,,,0, +67824,,,,,,,,0, +8757,,,,,,,,0, +1021,,,,,,,,0, +28954,60.0,6.0,8.0,8.0,8.0,10.0,5.0,2,2.0 +1856,,,,,,,,0, +10424,,,,,,,,0, +38584,,,,,,,,1,0.94 +62999,,,,,,,,0, +15205,90.0,9.0,9.0,9.0,8.0,10.0,10.0,2,2.0 +73847,,,,,,,,1,1.0 +5349,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +510,,,,,,,,0, +46880,,,,,,,,0, +51735,,,,,,,,0, +14671,100.0,9.0,9.0,10.0,10.0,10.0,10.0,2,2.0 +19181,,,,,,,,0, +535,,,,,,,,1, +28817,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,3.64 +7963,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +8181,80.0,8.0,8.0,10.0,10.0,8.0,10.0,1,1.0 +461,87.0,10.0,6.0,10.0,10.0,10.0,9.0,3,3.0 +710,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,3.0 +8998,90.0,10.0,10.0,9.0,10.0,10.0,10.0,4,4.0 +6449,93.0,10.0,10.0,10.0,10.0,9.0,9.0,9,9.0 +61062,,,,,,,,0, +65578,,,,,,,,0, +53495,,,,,,,,0, +65567,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +77076,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +10757,,,,,,,,0, +53427,,,,,,,,0, +45498,,,,,,,,0, +7537,60.0,4.0,8.0,10.0,10.0,10.0,4.0,1,1.0 +2917,,,,,,,,0, +57337,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +66949,,,,,,,,0, +17970,,,,,,,,0, +54263,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +21889,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,4.0 +68073,80.0,8.0,8.0,10.0,10.0,10.0,8.0,1,1.0 +57558,,,,,,,,0, +35242,,,,,,,,0, +63248,20.0,2.0,4.0,4.0,2.0,10.0,4.0,1,1.0 +22797,,,,,,,,0, +72958,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,3.0 +41704,,,,,,,,0, +53423,,,,,,,,0, +50611,,,,,,,,0, +35198,,,,,,,,0, +14005,,,,,,,,0, +47632,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +21816,100.0,10.0,10.0,10.0,10.0,10.0,9.0,3,3.0 +14964,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +17887,,,,,,,,0, +3443,,,,,,,,0, +68885,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +25834,73.0,7.0,8.0,7.0,7.0,5.0,7.0,3,3.0 +37917,,,,,,,,0, +29794,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +28796,,,,,,,,0, +8137,,,,,,,,0, +55439,,,,,,,,0, +74879,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +21050,,,,,,,,0, +21684,,,,,,,,1,1.0 +71898,,,,,,,,0, +54773,,,,,,,,0, +5691,,,,,,,,0, +20950,,,,,,,,0, +29347,100.0,10.0,10.0,10.0,10.0,10.0,8.0,2,2.0 +42634,84.0,9.0,9.0,9.0,8.0,8.0,9.0,5,5.0 +35598,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +54925,,,,,,,,0, +30096,,,,,,,,0, +71160,,,,,,,,0, +56853,,,,,,,,0, +5395,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +76614,95.0,10.0,10.0,9.0,10.0,9.0,10.0,4,4.0 +28819,87.0,9.0,8.0,10.0,10.0,9.0,9.0,3,3.0 +37524,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,1.0 +36918,,,,,,,,0, +48948,,,,,,,,0, +2181,,,,,,,,0, +32953,,,,,,,,0, +53891,87.0,9.0,8.0,8.0,9.0,10.0,9.0,3,3.0 +76247,,,,,,,,0, +14073,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +65607,,,,,,,,0, +44367,,,,,,,,0, +23544,,,,,,,,0, +6886,100.0,10.0,10.0,10.0,10.0,9.0,10.0,3,3.0 +9194,,,,,,,,0, +61036,,,,,,,,0, +49967,,,,,,,,0, +28688,,,,,,,,0, +74020,,,,,,,,0, +32403,90.0,9.0,9.0,10.0,9.0,6.0,9.0,2,2.0 +71651,,,,,,,,0, +51002,,,,,,,,0, +48549,,,,,,,,0, +71010,,,,,,,,0, +61297,,,,,,,,0, +5208,,,,,,,,0, +10891,,,,,,,,0, +76670,60.0,10.0,6.0,10.0,8.0,8.0,6.0,1,1.0 +68093,,,,,,,,0, +30219,,,,,,,,0, +11565,,,,,,,,0, +26673,80.0,9.0,10.0,10.0,8.0,10.0,8.0,2,2.0 +41253,20.0,2.0,4.0,2.0,2.0,6.0,2.0,1,1.0 +50468,60.0,10.0,10.0,10.0,6.0,10.0,6.0,1,1.0 +7151,100.0,10.0,10.0,9.0,10.0,10.0,10.0,2,2.0 +52760,,,,,,,,0, +28144,,,,,,,,0, +36906,,,,,,,,0, +19021,,,,,,,,0, +62895,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +34299,,,,,,,,1,1.0 +20184,,,,,,,,1, +76070,,,,,,,,0, +73014,100.0,10.0,10.0,10.0,8.0,10.0,10.0,1,1.0 +41314,,,,,,,,0, +69065,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +69474,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +5764,100.0,9.0,9.0,10.0,10.0,9.0,9.0,3,3.0 +33111,,,,,,,,0, +58756,,,,,,,,0, +11550,,,,,,,,0, +55916,,,,,,,,0, +43287,,,,,,,,0, +41425,,,,,,,,1,1.0 +62373,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +27256,,,,,,,,1,1.0 +55749,,,,,,,,0, +11797,,,,,,,,0, +66384,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +42033,80.0,10.0,10.0,10.0,6.0,8.0,8.0,1,1.0 +10322,,,,,,,,0, +1832,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,3.0 +54388,,,,,,,,0, +16530,,,,,,,,0, +10146,,,,,,,,0, +53976,,,,,,,,0, +3369,100.0,10.0,9.0,10.0,10.0,10.0,10.0,2,2.0 +6970,,,,,,,,0, +66580,,,,,,,,0, +25575,,,,,,,,0, +43034,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +6889,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +35914,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,1.0 +41147,,,,,,,,0, +15837,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +31768,,,,,,,,0, +42958,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +60357,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +36650,,,,,,,,0, +71606,,,,,,,,0, +59632,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,3.0 +45986,100.0,10.0,10.0,10.0,10.0,9.0,8.0,2,2.0 +26799,,,,,,,,0, +48809,100.0,10.0,8.0,10.0,8.0,10.0,10.0,1,1.0 +28116,90.0,10.0,9.0,10.0,9.0,10.0,9.0,4,4.0 +21797,,,,,,,,0, +34048,,,,,,,,0, +1879,,,,,,,,0, +37162,60.0,6.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +227,,,,,,,,1,1.0 +287,,,,,,,,0, +62044,90.0,10.0,10.0,9.0,10.0,10.0,9.0,2,2.0 +16269,,,,,,,,0, +66639,,,,,,,,0, +45720,,,,,,,,0, +26263,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +52080,,,,,,,,0, +46235,90.0,8.0,8.0,10.0,10.0,10.0,9.0,2,2.0 +52312,80.0,10.0,10.0,10.0,10.0,10.0,8.0,2,2.0 +48140,100.0,9.0,10.0,10.0,10.0,10.0,10.0,4,4.0 +18314,,,,,,,,0, +963,80.0,7.0,9.0,9.0,10.0,9.0,9.0,2,2.0 +1726,,,,,,,,0, +72587,,,,,,,,0, +11860,,,,,,,,0, +5862,,,,,,,,0, +26653,,,,,,,,0, +63027,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +15052,,,,,,,,0, +16719,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +64862,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,2.0 +71344,,,,,,,,0, +35339,100.0,8.0,8.0,10.0,8.0,8.0,8.0,1,1.0 +39153,60.0,8.0,4.0,6.0,8.0,6.0,6.0,1,1.0 +20924,,,,,,,,0, +13103,,,,,,,,0, +50093,,,,,,,,0, +19624,100.0,10.0,8.0,10.0,10.0,10.0,10.0,2,2.0 +73175,,,,,,,,0, +73965,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +7618,,,,,,,,0, +53885,,,,,,,,0, +11301,,,,,,,,0, +53299,,,,,,,,0, +53609,100.0,10.0,10.0,10.0,10.0,6.0,10.0,1,1.0 +70184,,,,,,,,0, +38192,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +46786,,,,,,,,0, +21212,,,,,,,,2,2.0 +1606,40.0,2.0,6.0,2.0,4.0,6.0,6.0,1,1.0 +72565,,,,,,,,0, +8160,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +1271,,,,,,,,0, +12328,,,,,,,,0, +2944,,,,,,,,0, +42805,80.0,8.0,8.0,10.0,10.0,10.0,10.0,1,1.0 +57238,,,,,,,,0, +43843,,,,,,,,0, +9124,,,,,,,,0, +49927,,,,,,,,0, +72960,,,,,,,,1, +44928,,,,,,,,0, +63755,,,,,,,,0, +41195,,,,,,,,0, +64533,,,,,,,,0, +15315,87.0,9.0,9.0,10.0,10.0,10.0,9.0,3,3.0 +71373,,,,,,,,1,1.0 +17412,,,,,,,,0, +41895,,,,,,,,0, +59876,,,,,,,,0, +10791,,,,,,,,0, +73960,,,,,,,,0, +49817,,,,,,,,0, +53859,,,,,,,,0, +51763,,,,,,,,0, +45903,,,,,,,,0, +12829,80.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +24758,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,4.0 +18760,,,,,,,,1,1.0 +52677,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,3.0 +63792,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +14161,100.0,9.0,10.0,9.0,10.0,9.0,9.0,3,3.0 +19310,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +70671,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +22856,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +18953,,,,,,,,0, +21702,,,,,,,,0, +74642,,,,,,,,0, +14110,,,,,,,,0, +34682,80.0,9.0,9.0,10.0,10.0,9.0,8.0,2,2.0 +32482,,,,,,,,0, +74758,,,,,,,,0, +45436,,,,,,,,0, +34312,,,,,,,,0, +4095,,,,,,,,0, +41661,100.0,10.0,10.0,8.0,10.0,10.0,10.0,1,1.0 +35548,,,,,,,,0, +23645,,,,,,,,1,1.0 +56747,,,,,,,,0, +45304,,,,,,,,0, +65155,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +12367,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,6.0 +53311,95.0,10.0,9.0,8.0,10.0,10.0,9.0,4,4.0 +15322,,,,,,,,0, +45713,,,,,,,,0, +30717,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,7.0 +27877,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +41209,80.0,8.0,6.0,10.0,10.0,8.0,6.0,1,1.0 +72204,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +71664,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,1.0 +39384,70.0,10.0,8.0,10.0,10.0,8.0,9.0,2,2.0 +53429,,,,,,,,0, +63525,,,,,,,,0, +27886,,,,,,,,0, +33003,,,,,,,,0, +38236,,,,,,,,0, +38756,,,,,,,,0, +66382,,,,,,,,0, +73690,,,,,,,,0, +53366,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +60010,,,,,,,,0, +39120,,,,,,,,0, +55541,,,,,,,,0, +40757,,,,,,,,0, +18881,,,,,,,,1,1.0 +7229,,,,,,,,0, +32812,,,,,,,,0, +21734,,,,,,,,0, +38123,,,,,,,,0, +13239,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +75877,,,,,,,,0, +65791,,,,,,,,0, +24510,,,,,,,,0, +47378,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +11756,90.0,9.0,8.0,7.0,8.0,8.0,8.0,2,2.0 +38665,100.0,8.0,8.0,10.0,8.0,8.0,8.0,1,1.0 +6680,,,,,,,,0, +45538,,,,,,,,0, +23655,,,,,,,,0, +8782,,,,,,,,0, +15679,,,,,,,,0, +26981,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +40800,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +29732,75.0,8.0,8.0,8.0,8.0,8.0,8.0,4,4.0 +15151,,,,,,,,0, +6513,,,,,,,,0, +14446,,,,,,,,0, +27927,,,,,,,,1, +50305,90.0,10.0,9.0,10.0,9.0,10.0,9.0,2,2.0 +34963,,,,,,,,0, +4165,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +12816,,,,,,,,0, +6110,,,,,,,,0, +68512,,,,,,,,0, +4556,60.0,8.0,8.0,10.0,10.0,8.0,6.0,1,1.0 +37346,,,,,,,,0, +61143,,,,,,,,0, +60199,,,,,,,,0, +21908,,,,,,,,0, +16047,,,,,,,,0, +3607,,,,,,,,0, +15172,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,3.0 +14366,,,,,,,,0, +50194,,,,,,,,0, +56206,100.0,9.0,10.0,8.0,9.0,9.0,9.0,2,2.0 +57065,,,,,,,,0, +55449,,,,,,,,0, +53123,,,,,,,,0, +68741,,,,,,,,0, +19170,,,,,,,,0, +13430,,,,,,,,0, +65686,,,,,,,,0, +36156,,,,,,,,0, +40897,,,,,,,,0, +16732,,,,,,,,0, +47040,,,,,,,,0, +64600,,,,,,,,0, +62617,,,,,,,,0, +33312,,,,,,,,1,1.0 +48030,,,,,,,,0, +44741,,,,,,,,0, +49747,,,,,,,,0, +42102,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,1.0 +32000,,,,,,,,0, +35483,,,,,,,,0, +53739,,,,,,,,0, +60367,,,,,,,,0, +57753,,,,,,,,0, +42160,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +2104,90.0,9.0,10.0,10.0,10.0,9.0,9.0,2,2.0 +48644,,,,,,,,0, +60374,,,,,,,,0, +70487,,,,,,,,0, +60633,,,,,,,,0, +16072,,,,,,,,0, +71238,,,,,,,,0, +56146,,,,,,,,0, +57021,,,,,,,,0, +74624,,,,,,,,0, +64294,,,,,,,,0, +10359,,,,,,,,0, +22071,,,,,,,,0, +27909,,,,,,,,0, +73226,,,,,,,,0, +48649,,,,,,,,0, +6944,,,,,,,,0, +68766,,,,,,,,1,1.0 +76459,,,,,,,,0, +23606,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +2029,100.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +13793,100.0,10.0,10.0,10.0,10.0,8.0,8.0,1,1.0 +5137,,,,,,,,0, +4345,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +74969,,,,,,,,0, +11563,,,,,,,,0, +15047,,,,,,,,0, +30897,,,,,,,,0, +4021,,,,,,,,0, +30941,90.0,10.0,10.0,9.0,8.0,10.0,9.0,2,2.0 +17052,100.0,10.0,10.0,9.0,10.0,10.0,9.0,3,3.0 +73190,,,,,,,,1,1.0 +66936,,,,,,,,1, +41372,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +15472,,,,,,,,0, +10989,,,,,,,,0, +64975,,,,,,,,1,1.0 +32450,,,,,,,,0, +11828,,,,,,,,0, +45457,,,,,,,,0, +45176,,,,,,,,0, +37312,,,,,,,,0, +50659,,,,,,,,0, +46694,,,,,,,,0, +70451,,,,,,,,0, +55287,,,,,,,,0, +39933,,,,,,,,0, +59075,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,6.0 +51511,,,,,,,,0, +20225,,,,,,,,0, +22766,100.0,10.0,10.0,10.0,10.0,10.0,10.0,7,7.0 +3160,,,,,,,,0, +43712,,,,,,,,0, +12447,,,,,,,,0, +16537,,,,,,,,0, +62636,,,,,,,,0, +47773,,,,,,,,1,1.0 +11091,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,3.0 +59451,80.0,10.0,6.0,10.0,10.0,8.0,8.0,1,1.0 +18340,,,,,,,,0, +70489,80.0,7.0,8.0,8.0,9.0,10.0,10.0,2,2.0 +7113,,,,,,,,0, +5979,,,,,,,,0, +16985,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +46699,,,,,,,,0, +36745,100.0,10.0,10.0,10.0,10.0,10.0,10.0,4,4.0 +46592,,,,,,,,0, +69834,100.0,10.0,10.0,10.0,10.0,8.0,10.0,1,1.0 +62541,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +4751,,,,,,,,0, +45320,,,,,,,,0, +10283,,,,,,,,0, +41248,,,,,,,,0, +5517,,,,,,,,0, +7099,100.0,10.0,10.0,10.0,10.0,9.0,9.0,3,3.0 +3586,,,,,,,,2, +374,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +10034,80.0,8.0,10.0,8.0,8.0,10.0,10.0,1,1.0 +33173,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +70433,,,,,,,,0, +22860,,,,,,,,0, +62056,,,,,,,,0, +61847,,,,,,,,0, +64445,,,,,,,,0, +38241,,,,,,,,0, +27957,80.0,10.0,10.0,10.0,10.0,10.0,8.0,1,1.0 +52751,,,,,,,,0, +72266,,,,,,,,1,1.0 +30229,100.0,8.0,8.0,2.0,8.0,10.0,8.0,1,1.0 +45314,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +29369,,,,,,,,0, +31888,,,,,,,,0, +16312,,,,,,,,0, +60796,,,,,,,,1,1.0 +37986,,,,,,,,0, +65305,,,,,,,,0, +45646,,,,,,,,0, +42852,,,,,,,,0, +23111,100.0,8.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +45613,,,,,,,,0, +36198,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +50938,,,,,,,,0, +33898,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +65409,,,,,,,,0, +68484,,,,,,,,0, +35216,,,,,,,,1,1.0 +60910,,,,,,,,0, +46103,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +5323,,,,,,,,0, +25902,,,,,,,,0, +38423,,,,,,,,0, +53154,,,,,,,,0, +69241,,,,,,,,0, +24793,,,,,,,,0, +42267,,,,,,,,0, +43240,,,,,,,,0, +37460,100.0,10.0,9.0,10.0,10.0,10.0,9.0,3,3.0 +31517,,,,,,,,0, +15493,,,,,,,,0, +30924,,,,,,,,0, +76960,,,,,,,,0, +18172,,,,,,,,0, +28025,,,,,,,,0, +73374,,,,,,,,0, +15921,,,,,,,,0, +59852,,,,,,,,0, +45134,,,,,,,,0, +13715,,,,,,,,0, +36387,,,,,,,,1,1.0 +12302,,,,,,,,0, +19216,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +63619,,,,,,,,1,1.0 +15884,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +52821,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +4580,,,,,,,,0, +17008,,,,,,,,0, +32198,,,,,,,,0, +51687,,,,,,,,0, +25301,,,,,,,,0, +29965,,,,,,,,0, +71735,,,,,,,,0, +75453,,,,,,,,0, +6343,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +12493,,,,,,,,0, +3502,,,,,,,,0, +15357,,,,,,,,0, +6671,,,,,,,,0, +48681,,,,,,,,0, +70320,,,,,,,,0, +11628,,,,,,,,0, +63721,,,,,,,,0, +56780,,,,,,,,0, +31037,,,,,,,,0, +23701,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +46999,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +43754,,,,,,,,0, +76536,,,,,,,,0, +63566,,,,,,,,0, +30898,,,,,,,,0, +40420,,,,,,,,0, +46923,,,,,,,,0, +26620,90.0,9.0,9.0,9.0,9.0,10.0,10.0,2,2.0 +22342,,,,,,,,0, +47679,,,,,,,,0, +12216,,,,,,,,0, +23036,,,,,,,,0, +27348,,,,,,,,0, +19391,,,,,,,,0, +39799,,,,,,,,0, +15848,,,,,,,,0, +53738,80.0,10.0,8.0,10.0,10.0,8.0,8.0,1,1.0 +53107,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +65366,,,,,,,,0, +28671,,,,,,,,0, +38597,,,,,,,,0, +65890,,,,,,,,0, +39340,,,,,,,,0, +43278,,,,,,,,0, +24634,,,,,,,,0, +56015,,,,,,,,0, +9411,60.0,10.0,8.0,10.0,10.0,10.0,8.0,1,1.0 +65889,100.0,10.0,10.0,10.0,10.0,10.0,10.0,6,6.0 +42088,,,,,,,,0, +72687,,,,,,,,0, +3707,90.0,9.0,9.0,10.0,10.0,10.0,10.0,2,2.0 +73476,,,,,,,,0, +47426,,,,,,,,0, +34427,100.0,10.0,10.0,10.0,9.0,10.0,10.0,2,2.0 +38082,,,,,,,,0, +66470,,,,,,,,0, +66857,,,,,,,,0, +40976,,,,,,,,0, +19361,,,,,,,,0, +29361,,,,,,,,0, +66875,,,,,,,,0, +37004,,,,,,,,0, +34021,,,,,,,,0, +33789,,,,,,,,0, +64798,,,,,,,,0, +26369,,,,,,,,0, +71426,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +37152,,,,,,,,0, +42882,,,,,,,,0, +10505,,,,,,,,0, +3793,,,,,,,,0, +75808,,,,,,,,0, +56954,,,,,,,,0, +2571,,,,,,,,0, +12547,,,,,,,,0, +28320,,,,,,,,0, +56007,,,,,,,,0, +58154,,,,,,,,0, +54271,,,,,,,,0, +34949,,,,,,,,0, +73966,,,,,,,,0, +37969,,,,,,,,0, +16817,,,,,,,,0, +55687,,,,,,,,0, +45769,,,,,,,,0, +29628,,,,,,,,0, +37559,,,,,,,,0, +59723,,,,,,,,0, +23401,,,,,,,,0, +19882,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +73201,,,,,,,,0, +40457,,,,,,,,0, +54480,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +36805,,,,,,,,0, +24210,,,,,,,,0, +63020,,,,,,,,0, +17044,,,,,,,,0, +40906,,,,,,,,0, +58320,90.0,9.0,8.0,10.0,10.0,9.0,10.0,2,2.0 +40223,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +75614,,,,,,,,1,1.0 +67757,,,,,,,,0, +5019,,,,,,,,0, +5779,,,,,,,,0, +66179,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +74507,,,,,,,,0, +36679,,,,,,,,0, +47905,,,,,,,,0, +68205,,,,,,,,0, +49908,,,,,,,,1,1.0 +53246,,,,,,,,0, +73442,,,,,,,,0, +69838,,,,,,,,0, +29334,100.0,10.0,10.0,10.0,10.0,8.0,10.0,2,2.0 +38024,,,,,,,,0, +8180,,,,,,,,0, +67414,,,,,,,,0, +28125,,,,,,,,0, +7152,,,,,,,,0, +11202,,,,,,,,0, +4325,,,,,,,,0, +21632,,,,,,,,0, +51502,,,,,,,,0, +43289,,,,,,,,0, +524,,,,,,,,0, +27308,,,,,,,,0, +45405,,,,,,,,0, +49839,100.0,10.0,10.0,10.0,10.0,10.0,10.0,2,2.0 +4357,,,,,,,,0, +65756,,,,,,,,0, +39182,,,,,,,,0, +56182,,,,,,,,0, +38098,,,,,,,,0, +74436,,,,,,,,0, +28786,,,,,,,,0, +59920,,,,,,,,0, +36159,,,,,,,,0, +31744,,,,,,,,0, +2452,,,,,,,,0, +63932,,,,,,,,0, +19043,,,,,,,,0, +61123,,,,,,,,0, +28264,,,,,,,,0, +59798,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,3.0 +68365,,,,,,,,0, +26794,,,,,,,,0, +29777,,,,,,,,0, +10055,,,,,,,,0, +25978,,,,,,,,0, +54916,,,,,,,,0, +16566,,,,,,,,0, +64438,,,,,,,,0, +23620,,,,,,,,0, +39169,,,,,,,,1, +50747,,,,,,,,0, +61706,,,,,,,,0, +51819,,,,,,,,0, +53695,,,,,,,,0, +73579,,,,,,,,0, +5110,,,,,,,,0, +53746,,,,,,,,0, +71870,,,,,,,,0, +53108,,,,,,,,0, +68832,,,,,,,,0, +10256,,,,,,,,0, +4272,,,,,,,,0, +75218,,,,,,,,0, +28873,,,,,,,,0, +389,,,,,,,,0, +9723,,,,,,,,0, +57931,,,,,,,,0, +7231,,,,,,,,0, +18274,,,,,,,,0, +72123,,,,,,,,0, +35570,,,,,,,,0, +1926,,,,,,,,0, +54957,,,,,,,,0, +14351,,,,,,,,0, +63088,,,,,,,,0, +71438,,,,,,,,0, +38791,,,,,,,,0, +24937,,,,,,,,0, +14233,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,3.0 +5712,,,,,,,,0, +30417,,,,,,,,0, +4112,,,,,,,,0, +70326,,,,,,,,1,1.0 +39749,,,,,,,,0, +21323,,,,,,,,0, +26411,,,,,,,,0, +47884,,,,,,,,0, +605,,,,,,,,0, +73831,,,,,,,,0, +252,,,,,,,,0, +73580,,,,,,,,0, +14263,,,,,,,,0, +53572,,,,,,,,0, +76595,,,,,,,,0, +75683,,,,,,,,0, +20330,100.0,10.0,10.0,10.0,10.0,10.0,10.0,1,1.0 +74280,,,,,,,,0, +3852,,,,,,,,0, +4636,,,,,,,,0, +9787,,,,,,,,0, +66410,,,,,,,,0, +58651,,,,,,,,0, +45337,,,,,,,,0, +33553,,,,,,,,0, +942,,,,,,,,0, +16445,100.0,10.0,10.0,10.0,10.0,10.0,10.0,3,3.0 +39064,,,,,,,,0, +3720,,,,,,,,0, +49185,,,,,,,,0, +63189,,,,,,,,0, +61578,,,,,,,,0, +16555,,,,,,,,0, +28739,,,,,,,,1,1.0 +54335,,,,,,,,0, +48725,,,,,,,,1,1.0 +32382,,,,,,,,1,1.0 +11783,,,,,,,,0, +40900,,,,,,,,1,1.0 +55465,,,,,,,,1,1.0 +36844,,,,,,,,0, +77091,,,,,,,,0, +4697,,,,,,,,0, +73179,,,,,,,,0, +9187,,,,,,,,0, +68985,,,,,,,,0, +12205,,,,,,,,0, +30883,,,,,,,,0, +35307,,,,,,,,1,1.0 +25260,,,,,,,,0, +71273,,,,,,,,0, +26162,,,,,,,,1, +9371,,,,,,,,0, +15568,,,,,,,,0, +16123,,,,,,,,0, +49577,,,,,,,,0, +3816,,,,,,,,0, +61722,,,,,,,,0, +12467,,,,,,,,0, +9009,,,,,,,,0, +23072,,,,,,,,0, +6571,,,,,,,,0, +23760,,,,,,,,0, +59233,,,,,,,,0, +1572,,,,,,,,0, +25894,,,,,,,,0, +27076,,,,,,,,0, +48485,,,,,,,,0, +4268,,,,,,,,0, +8484,,,,,,,,0, +44001,,,,,,,,0, +44081,,,,,,,,0, +65179,,,,,,,,0, +2617,,,,,,,,0, +67867,,,,,,,,0, +60177,,,,,,,,0, +28861,,,,,,,,0, +6436,,,,,,,,0, +44797,,,,,,,,0, +63249,,,,,,,,0, +40917,,,,,,,,0, +253,,,,,,,,0, +43954,,,,,,,,0, +74534,,,,,,,,0, +35468,,,,,,,,0, +49782,,,,,,,,0, +63637,,,,,,,,0, +72538,,,,,,,,0, +52871,,,,,,,,0, +52946,,,,,,,,0, +43129,,,,,,,,0, +51874,,,,,,,,0, +53507,,,,,,,,0, +20407,,,,,,,,0, +49049,,,,,,,,0, +19963,,,,,,,,0, +2508,,,,,,,,0, +41263,,,,,,,,0, +9370,,,,,,,,0, +66968,,,,,,,,0, +47972,,,,,,,,0, +682,,,,,,,,0, +60880,,,,,,,,0, +67742,,,,,,,,0, +38360,,,,,,,,0, +69710,,,,,,,,0, +70928,,,,,,,,0, +12491,,,,,,,,0, +43532,,,,,,,,0, +18858,,,,,,,,0, +33340,,,,,,,,0, +30277,,,,,,,,0, +15049,,,,,,,,0, +10978,,,,,,,,0, +45764,,,,,,,,1,1.0 +66611,,,,,,,,0, +70284,,,,,,,,0, +57097,,,,,,,,0, +16569,,,,,,,,0, +16416,,,,,,,,0, +34863,,,,,,,,0, +31332,,,,,,,,0, +54082,,,,,,,,0, +32698,,,,,,,,0, +2137,,,,,,,,0, +43306,,,,,,,,0, +18144,,,,,,,,1,1.0 +46387,,,,,,,,0, +14586,80.0,10.0,10.0,10.0,8.0,10.0,8.0,2,2.0 +63735,,,,,,,,0, +6727,,,,,,,,0, +59178,,,,,,,,0, +67779,,,,,,,,0, +37221,,,,,,,,0, +57035,,,,,,,,0, +49680,,,,,,,,0, +54011,,,,,,,,1,1.0 +9492,,,,,,,,0, +12532,,,,,,,,0, +58823,,,,,,,,0, +9193,,,,,,,,0, +24603,,,,,,,,0, +3405,,,,,,,,0, +13338,,,,,,,,0, +39740,,,,,,,,0, +32329,,,,,,,,0, +54205,,,,,,,,0, +67651,,,,,,,,0, +75860,,,,,,,,0, +61780,,,,,,,,1, +40959,,,,,,,,0, +41662,,,,,,,,0, +57741,,,,,,,,0, +8585,,,,,,,,0, +23694,,,,,,,,0, +21395,,,,,,,,0, +75568,,,,,,,,0, +15812,,,,,,,,0, +57445,,,,,,,,0, +38796,,,,,,,,0, +76329,,,,,,,,0, +66331,,,,,,,,0, +72351,,,,,,,,0, +4961,,,,,,,,0, +49522,,,,,,,,0, +27382,,,,,,,,0, +3017,,,,,,,,0, +71447,,,,,,,,0, +62303,,,,,,,,0, +40110,,,,,,,,0, +15855,,,,,,,,0, +63074,,,,,,,,0, +22259,,,,,,,,0, +8307,,,,,,,,0, +45814,,,,,,,,0, +38094,,,,,,,,0, +56513,,,,,,,,0, +69550,,,,,,,,0, +15308,,,,,,,,0, +47157,,,,,,,,0, +12916,,,,,,,,0, +11267,,,,,,,,0, +10252,,,,,,,,0, +52004,,,,,,,,0, +19866,,,,,,,,0, +6533,,,,,,,,0, +17840,,,,,,,,0, +45960,,,,,,,,0, +9979,,,,,,,,0, +45177,,,,,,,,0, +60844,,,,,,,,0, +12430,,,,,,,,0, +66302,,,,,,,,0, +46207,,,,,,,,0, +14966,,,,,,,,0, +74262,,,,,,,,0, +27867,,,,,,,,0, +53828,,,,,,,,0, +71506,,,,,,,,0, +50683,,,,,,,,0, +31565,,,,,,,,0, +18394,,,,,,,,0, +66807,,,,,,,,0, +40832,,,,,,,,0, +39945,,,,,,,,0, +65137,,,,,,,,0, +6306,,,,,,,,0, +7916,,,,,,,,0, +7788,,,,,,,,0, +30450,,,,,,,,0, +30239,,,,,,,,0, +23565,,,,,,,,0, +11844,,,,,,,,0, +51643,,,,,,,,0, +68198,,,,,,,,0, +6069,,,,,,,,0, +32746,,,,,,,,0, +12065,,,,,,,,0, +40621,,,,,,,,0, +1307,,,,,,,,0, +29615,,,,,,,,0, +37063,,,,,,,,0, +47925,,,,,,,,0, +4145,,,,,,,,0, +72875,,,,,,,,0, +5109,,,,,,,,0, +12800,,,,,,,,0, +56550,,,,,,,,0, +64493,,,,,,,,0, +58366,,,,,,,,0, +64812,,,,,,,,0, +29235,,,,,,,,0, +19141,,,,,,,,0, +62201,,,,,,,,0, +23287,,,,,,,,0, +60889,,,,,,,,0, +44264,,,,,,,,0, +61886,,,,,,,,0, +63888,,,,,,,,0, +70401,,,,,,,,0, +19867,,,,,,,,0, +35568,,,,,,,,0, +35402,,,,,,,,0, +45518,,,,,,,,0, +29773,,,,,,,,0, +53650,,,,,,,,0, +75674,,,,,,,,1,1.0 +35660,,,,,,,,0, +11752,,,,,,,,0, +69498,,,,,,,,0, +532,,,,,,,,0, +72368,,,,,,,,0, +68743,,,,,,,,0, +3744,,,,,,,,0, +27793,,,,,,,,0, +30467,,,,,,,,0, +1020,,,,,,,,0, +2124,,,,,,,,0, +54425,,,,,,,,0, +21612,,,,,,,,0, +2794,,,,,,,,0, +70610,,,,,,,,0, +14305,,,,,,,,0, +18588,,,,,,,,0, +34004,,,,,,,,0, +69344,,,,,,,,0, +68022,,,,,,,,1,1.0 +15743,,,,,,,,0, +40489,,,,,,,,0, +62407,,,,,,,,0, +61738,,,,,,,,0, +20426,,,,,,,,0, +44325,,,,,,,,0, +8245,,,,,,,,0, +13686,,,,,,,,0, +47822,,,,,,,,0, +28355,,,,,,,,0, +32960,,,,,,,,0, +34363,,,,,,,,0, +27900,,,,,,,,0, +58984,,,,,,,,0, +19726,,,,,,,,0, +66909,,,,,,,,0, +590,,,,,,,,0, +10858,,,,,,,,0, +44083,,,,,,,,0, +48920,,,,,,,,0, +63513,,,,,,,,0, +44668,,,,,,,,0, +4368,,,,,,,,0, +21738,,,,,,,,0, +72645,,,,,,,,0, diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/shuttles.xlsx b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/shuttles.xlsx new file mode 100644 index 00000000..cd48599b Binary files /dev/null and b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Datasets/shuttles.xlsx differ diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Schemas/CompanySchema.cs b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Schemas/CompanySchema.cs new file mode 100644 index 00000000..0db3cbd7 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Schemas/CompanySchema.cs @@ -0,0 +1,19 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._01_Raw.Schemas; + +[FlowthruSchema] +public partial record CompanySchema +{ + [SerializedLabel("id")] + public string Id { get; init; } = null!; + + [SerializedLabel("company_rating")] + public string CompanyRating { get; init; } = null!; + + [SerializedLabel("iata_approved")] + public string IataApproved { get; init; } = null!; + + [SerializedLabel("company_location")] + public string CompanyLocation { get; init; } = null!; +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Schemas/ReviewSchema.cs b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Schemas/ReviewSchema.cs new file mode 100644 index 00000000..d6b6d9bc --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Schemas/ReviewSchema.cs @@ -0,0 +1,13 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._01_Raw.Schemas; + +[FlowthruSchema] +public partial record ReviewSchema +{ + [SerializedLabel("shuttle_id")] + public string ShuttleId { get; init; } = null!; + + [SerializedLabel("review_scores_rating")] + public string ReviewScoresRating { get; init; } = null!; +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Schemas/ShuttleSchema.cs b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Schemas/ShuttleSchema.cs new file mode 100644 index 00000000..4e42efca --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_01_Raw/Schemas/ShuttleSchema.cs @@ -0,0 +1,34 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._01_Raw.Schemas; + +[FlowthruSchema] +public partial record ShuttleSchema +{ + [SerializedLabel("id")] + public string Id { get; init; } = null!; + + [SerializedLabel("shuttle_type")] + public string ShuttleType { get; init; } = null!; + + [SerializedLabel("company_id")] + public string CompanyId { get; init; } = null!; + + [SerializedLabel("engines")] + public string Engines { get; init; } = null!; + + [SerializedLabel("passenger_capacity")] + public string PassengerCapacity { get; init; } = null!; + + [SerializedLabel("crew")] + public string Crew { get; init; } = null!; + + [SerializedLabel("price")] + public string Price { get; init; } = null!; + + [SerializedLabel("d_check_complete")] + public string DCheckComplete { get; init; } = null!; + + [SerializedLabel("moon_clearance_complete")] + public string MoonClearanceComplete { get; init; } = null!; +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Catalog.Intermediate.cs b/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Catalog.Intermediate.cs new file mode 100644 index 00000000..9c0bad87 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Catalog.Intermediate.cs @@ -0,0 +1,27 @@ +using Flowthru.Core.Data; +using Flowthru.Extensions.Spark; +using Flowthru.Misc.DataFrames; +using KedroSpaceflightsSpark.Data._02_Intermediate.Schemas; +using SparkFactory = Flowthru.Extensions.Spark.ItemFactory; + +namespace KedroSpaceflightsSpark.Data; + +/// +/// Intermediate data layer: preprocessed typed datasets held as deferred Spark execution plans. +/// All items are in-memory TypedFrames — no file persistence at this layer. +/// +public partial class Catalog +{ + public IItem> PreprocessedCompanies => + CreateItem( + () => SparkFactory.Frame.Memory(label: "PreprocessedCompanies") + ); + + public IItem> PreprocessedShuttles => + CreateItem( + () => SparkFactory.Frame.Memory(label: "PreprocessedShuttles") + ); + + public IItem> ParsedReviews => + CreateItem(() => SparkFactory.Frame.Memory(label: "ParsedReviews")); +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Schemas/ParsedReviewSchema.cs b/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Schemas/ParsedReviewSchema.cs new file mode 100644 index 00000000..7e069398 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Schemas/ParsedReviewSchema.cs @@ -0,0 +1,17 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._02_Intermediate.Schemas; + +/// +/// Review data filtered to entries with valid numeric scores. +/// Produced by PreprocessReviewsStep; passed into the model input table join as a TypedFrame. +/// +[FlowthruSchema] +public partial record ParsedReviewSchema +{ + [SerializedLabel("shuttle_id")] + public required string ShuttleId { get; init; } + + [SerializedLabel("review_scores_rating")] + public required double ReviewScoresRating { get; init; } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Schemas/PreprocessedCompanySchema.cs b/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Schemas/PreprocessedCompanySchema.cs new file mode 100644 index 00000000..4ccbbff2 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Schemas/PreprocessedCompanySchema.cs @@ -0,0 +1,23 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._02_Intermediate.Schemas; + +/// +/// Preprocessed company data with strongly-typed fields. +/// Uses double for floating-point fields to match Spark's DoubleType columns. +/// +[FlowthruSchema] +public partial record PreprocessedCompanySchema +{ + [SerializedLabel("id")] + public required string Id { get; init; } + + [SerializedLabel("company_rating")] + public required double CompanyRating { get; init; } + + [SerializedLabel("iata_approved")] + public required bool IataApproved { get; init; } + + [SerializedLabel("company_location")] + public required string CompanyLocation { get; init; } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Schemas/PreprocessedShuttleSchema.cs b/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Schemas/PreprocessedShuttleSchema.cs new file mode 100644 index 00000000..bd1f0162 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_02_Intermediate/Schemas/PreprocessedShuttleSchema.cs @@ -0,0 +1,38 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._02_Intermediate.Schemas; + +/// +/// Preprocessed shuttle data with strongly-typed fields. +/// Uses double for floating-point fields to match Spark's DoubleType columns. +/// +[FlowthruSchema] +public partial record PreprocessedShuttleSchema +{ + [SerializedLabel("id")] + public required string Id { get; init; } + + [SerializedLabel("shuttle_type")] + public required string ShuttleType { get; init; } + + [SerializedLabel("company_id")] + public required string CompanyId { get; init; } + + [SerializedLabel("engines")] + public required int Engines { get; init; } + + [SerializedLabel("passenger_capacity")] + public required int PassengerCapacity { get; init; } + + [SerializedLabel("crew")] + public required int Crew { get; init; } + + [SerializedLabel("price")] + public required double Price { get; init; } + + [SerializedLabel("d_check_complete")] + public required bool DCheckComplete { get; init; } + + [SerializedLabel("moon_clearance_complete")] + public required bool MoonClearanceComplete { get; init; } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_03_Primary/Catalog.Primary.cs b/examples/starter/KedroSpaceflightsSpark/Data/_03_Primary/Catalog.Primary.cs new file mode 100644 index 00000000..74d62412 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_03_Primary/Catalog.Primary.cs @@ -0,0 +1,22 @@ +using Flowthru.Core.Data; +using KedroSpaceflightsSpark.Data._03_Primary.Schemas; + +namespace KedroSpaceflightsSpark.Data; + +public partial class Catalog +{ + /// + /// Unified model input table. Persisted to Parquet at this layer so the DataScience + /// flow can consume it as a materialized IEnumerable without requiring Spark. + /// The TypedFrame produced by CreateModelInputTableStep materializes implicitly + /// when the Parquet serializer enumerates it. + /// + public IItem> ModelInputTable => + CreateItem( + () => + ItemFactory.Enumerable.Parquet( + label: "ModelInputTable", + filePath: $"{_basePath}/_03_Primary/Datasets/model_input_table.parquet" + ) + ); +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_03_Primary/Schemas/ModelInputTableSchema.cs b/examples/starter/KedroSpaceflightsSpark/Data/_03_Primary/Schemas/ModelInputTableSchema.cs new file mode 100644 index 00000000..062d7c0f --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_03_Primary/Schemas/ModelInputTableSchema.cs @@ -0,0 +1,47 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._03_Primary.Schemas; + +/// +/// Unified model input table combining shuttle, company, and review data. +/// Uses double for floating-point fields to match Spark's DoubleType columns. +/// +[FlowthruSchema] +public partial record ModelInputTableSchema +{ + [SerializedLabel("shuttle_id")] + public required string ShuttleId { get; init; } + + [SerializedLabel("shuttle_type")] + public required string ShuttleType { get; init; } + + [SerializedLabel("company_id")] + public required string CompanyId { get; init; } + + [SerializedLabel("engines")] + public required int Engines { get; init; } + + [SerializedLabel("passenger_capacity")] + public required int PassengerCapacity { get; init; } + + [SerializedLabel("crew")] + public required int Crew { get; init; } + + [SerializedLabel("d_check_complete")] + public required bool DCheckComplete { get; init; } + + [SerializedLabel("moon_clearance_complete")] + public required bool MoonClearanceComplete { get; init; } + + [SerializedLabel("price")] + public required double Price { get; init; } + + [SerializedLabel("iata_approved")] + public required bool IataApproved { get; init; } + + [SerializedLabel("company_rating")] + public required double CompanyRating { get; init; } + + [SerializedLabel("review_scores_rating")] + public required double ReviewScoresRating { get; init; } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_03_Primary/Schemas/ShuttleReviewSchema.cs b/examples/starter/KedroSpaceflightsSpark/Data/_03_Primary/Schemas/ShuttleReviewSchema.cs new file mode 100644 index 00000000..b310725b --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_03_Primary/Schemas/ShuttleReviewSchema.cs @@ -0,0 +1,42 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._03_Primary.Schemas; + +/// +/// Intermediate join result combining preprocessed shuttle data with a review score. +/// Produced by joining TypedFrame<PreprocessedShuttleSchema> with +/// TypedFrame<ParsedReviewSchema>; consumed by the second join that adds company fields. +/// +[FlowthruSchema] +public partial record ShuttleReviewSchema +{ + [SerializedLabel("shuttle_id")] + public required string ShuttleId { get; init; } + + [SerializedLabel("shuttle_type")] + public required string ShuttleType { get; init; } + + [SerializedLabel("company_id")] + public required string CompanyId { get; init; } + + [SerializedLabel("engines")] + public required int Engines { get; init; } + + [SerializedLabel("passenger_capacity")] + public required int PassengerCapacity { get; init; } + + [SerializedLabel("crew")] + public required int Crew { get; init; } + + [SerializedLabel("price")] + public required double Price { get; init; } + + [SerializedLabel("d_check_complete")] + public required bool DCheckComplete { get; init; } + + [SerializedLabel("moon_clearance_complete")] + public required bool MoonClearanceComplete { get; init; } + + [SerializedLabel("review_scores_rating")] + public required double ReviewScoresRating { get; init; } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_04_Feature/Catalog.Feature.cs b/examples/starter/KedroSpaceflightsSpark/Data/_04_Feature/Catalog.Feature.cs new file mode 100644 index 00000000..0478086b --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_04_Feature/Catalog.Feature.cs @@ -0,0 +1,8 @@ +using Flowthru.Core.Data; + +namespace KedroSpaceflightsSpark.Data; + +public partial class Catalog +{ + // Feature datasets added here as the pipeline evolves. +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_05_ModelInput/Catalog.ModelInput.cs b/examples/starter/KedroSpaceflightsSpark/Data/_05_ModelInput/Catalog.ModelInput.cs new file mode 100644 index 00000000..a49f5fe7 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_05_ModelInput/Catalog.ModelInput.cs @@ -0,0 +1,13 @@ +using Flowthru.Core.Data; +using KedroSpaceflightsSpark.Data._05_ModelInput.Schemas; + +namespace KedroSpaceflightsSpark.Data; + +public partial class Catalog +{ + public IItem> TrainSplit => + CreateItem(() => ItemFactory.Enumerable.Memory(label: "XTrain")); + + public IItem> TestSplit => + CreateItem(() => ItemFactory.Enumerable.Memory(label: "XTest")); +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_05_ModelInput/Schemas/TestTrainSplit.cs b/examples/starter/KedroSpaceflightsSpark/Data/_05_ModelInput/Schemas/TestTrainSplit.cs new file mode 100644 index 00000000..accdca23 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_05_ModelInput/Schemas/TestTrainSplit.cs @@ -0,0 +1,25 @@ +namespace KedroSpaceflightsSpark.Data._05_ModelInput.Schemas; + +public record TrainingData +{ + public FeatureVector Features { get; init; } = null!; + public double Label { get; init; } +} + +public record TestData +{ + public FeatureVector Features { get; init; } = null!; + public double Label { get; init; } +} + +public record FeatureVector +{ + public int Engines { get; init; } + public int PassengerCapacity { get; init; } + public int Crew { get; init; } + public bool DCheckComplete { get; init; } + public bool MoonClearanceComplete { get; init; } + public bool IataApproved { get; init; } + public double CompanyRating { get; init; } + public double ReviewScoresRating { get; init; } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_06_Models/Catalog.Models.cs b/examples/starter/KedroSpaceflightsSpark/Data/_06_Models/Catalog.Models.cs new file mode 100644 index 00000000..185d502a --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_06_Models/Catalog.Models.cs @@ -0,0 +1,16 @@ +using Flowthru.Core.Data; +using KedroSpaceflightsSpark.Data._06_Models.Schemas; + +namespace KedroSpaceflightsSpark.Data; + +public partial class Catalog +{ + public IItem Regressor => + CreateItem( + () => + ItemFactory.Single.Json( + label: "Regressor", + filePath: $"{_basePath}/_06_Models/Datasets/regressor.json" + ) + ); +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_06_Models/Schemas/LinearRegressionModel.cs b/examples/starter/KedroSpaceflightsSpark/Data/_06_Models/Schemas/LinearRegressionModel.cs new file mode 100644 index 00000000..6b71997d --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_06_Models/Schemas/LinearRegressionModel.cs @@ -0,0 +1,11 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._06_Models.Schemas; + +[FlowthruSchema] +public partial record LinearRegressionModel +{ + public double[] Coefficients { get; init; } = Array.Empty(); + public double Intercept { get; init; } + public string[] FeatureNames { get; init; } = Array.Empty(); +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_07_ModelOutput/Catalog.ModelOutput.cs b/examples/starter/KedroSpaceflightsSpark/Data/_07_ModelOutput/Catalog.ModelOutput.cs new file mode 100644 index 00000000..45751e58 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_07_ModelOutput/Catalog.ModelOutput.cs @@ -0,0 +1,25 @@ +using Flowthru.Core.Data; +using KedroSpaceflightsSpark.Data._07_ModelOutput.Schemas; + +namespace KedroSpaceflightsSpark.Data; + +public partial class Catalog +{ + public IItem ModelMetrics => + CreateItem( + () => + ItemFactory.Single.Json( + label: "ModelMetrics", + filePath: $"{_basePath}/_07_ModelOutput/Datasets/model_metrics.json" + ) + ); + + public IItem> ModelPredictions => + CreateItem( + () => + ItemFactory.Enumerable.Json( + label: "ModelPredictions", + filePath: $"{_basePath}/_07_ModelOutput/Datasets/model_predictions.json" + ) + ); +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_07_ModelOutput/Schemas/ModelMetrics.cs b/examples/starter/KedroSpaceflightsSpark/Data/_07_ModelOutput/Schemas/ModelMetrics.cs new file mode 100644 index 00000000..bbb5db21 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_07_ModelOutput/Schemas/ModelMetrics.cs @@ -0,0 +1,11 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._07_ModelOutput.Schemas; + +[FlowthruSchema] +public partial record ModelMetrics +{ + public required decimal R2Score { get; init; } + public required decimal MeanAbsoluteError { get; init; } + public required decimal MaxError { get; init; } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_07_ModelOutput/Schemas/ModelPredictions.cs b/examples/starter/KedroSpaceflightsSpark/Data/_07_ModelOutput/Schemas/ModelPredictions.cs new file mode 100644 index 00000000..7b0ebdb3 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_07_ModelOutput/Schemas/ModelPredictions.cs @@ -0,0 +1,10 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._07_ModelOutput.Schemas; + +[FlowthruSchema] +public partial record ModelPredictions +{ + public double Actual { get; init; } + public double Predicted { get; init; } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_08_Reporting/Catalog.Reporting.cs b/examples/starter/KedroSpaceflightsSpark/Data/_08_Reporting/Catalog.Reporting.cs new file mode 100644 index 00000000..71d29ef2 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_08_Reporting/Catalog.Reporting.cs @@ -0,0 +1,43 @@ +using Flowthru.Core.Data; +using KedroSpaceflightsSpark.Data._08_Reporting.Schemas; +using Plotly.NET; + +namespace KedroSpaceflightsSpark.Data; + +public partial class Catalog +{ + public IItem> ShuttleCapacityReport => + CreateItem( + () => + ItemFactory.Enumerable.Json( + label: "ShuttleCapacityReport", + filePath: $"{_basePath}/_08_Reporting/Datasets/shuttle_capacity_report.json" + ) + ); + + public IItem ShuttlePassengerCapacityChart => + CreateItem( + () => ItemFactory.Single.Memory(label: "ShuttlePassengerCapacityChart") + ); + + public IItem ShuttlePassengerCapacityPlotPng => + CreateItem( + () => + ItemFactory.Single.Binary( + label: "ShuttlePassengerCapacityPlotPng", + filePath: $"{_basePath}/_08_Reporting/Images/shuttle_passenger_capacity_plot.png" + ) + ); + + public IItem ConfusionMatrixChart => + CreateItem(() => ItemFactory.Single.Memory(label: "ConfusionMatrixChart")); + + public IItem ConfusionMatrixPlotPng => + CreateItem( + () => + ItemFactory.Single.Binary( + label: "ConfusionMatrixPlotPng", + filePath: $"{_basePath}/_08_Reporting/Images/confusion_matrix_plot.png" + ) + ); +} diff --git a/examples/starter/KedroSpaceflightsSpark/Data/_08_Reporting/Schemas/ShuttleCapacityReport.cs b/examples/starter/KedroSpaceflightsSpark/Data/_08_Reporting/Schemas/ShuttleCapacityReport.cs new file mode 100644 index 00000000..b68e96b5 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Data/_08_Reporting/Schemas/ShuttleCapacityReport.cs @@ -0,0 +1,17 @@ +using Flowthru.Core.Abstractions; + +namespace KedroSpaceflightsSpark.Data._08_Reporting.Schemas; + +/// +/// Passenger capacity summary grouped by shuttle type. +/// Uses double for AvgPassengerCapacity to match Spark's DoubleType output from GroupBy.Aggregate. +/// +[FlowthruSchema] +public partial record ShuttleCapacityReport +{ + [SerializedLabel("shuttle_type")] + public required string ShuttleType { get; init; } + + [SerializedLabel("avg_passenger_capacity")] + public required double AvgPassengerCapacity { get; init; } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/DataProcessingFlow.cs b/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/DataProcessingFlow.cs new file mode 100644 index 00000000..9ba768a7 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/DataProcessingFlow.cs @@ -0,0 +1,49 @@ +using Flowthru.Core.Flows; +using KedroSpaceflightsSpark.Data; +using KedroSpaceflightsSpark.Flows.DataProcessing.Steps; + +namespace KedroSpaceflightsSpark.Flows.DataProcessing; + +public static class DataProcessingFlow +{ + public static Flow Create(Catalog catalog) + { + return FlowBuilder.CreateFlow(pipeline => + { + pipeline.AddStep( + label: "PreprocessCompanies", + description: "Parses raw company strings into a typed Spark DataFrame.", + transform: PreprocessCompaniesStep.Create(catalog.frameProvider), + input: catalog.Companies, + output: catalog.PreprocessedCompanies + ); + + pipeline.AddStep( + label: "PreprocessShuttles", + description: "Parses raw shuttle strings into a typed Spark DataFrame.", + transform: PreprocessShuttlesStep.Create(catalog.frameProvider), + input: catalog.Shuttles, + output: catalog.PreprocessedShuttles + ); + + pipeline.AddStep( + label: "PreprocessReviews", + description: "Filters reviews to valid numeric scores and loads them into a Spark DataFrame.", + transform: PreprocessReviewsStep.Create(catalog.frameProvider), + input: catalog.Reviews, + output: catalog.ParsedReviews + ); + + pipeline.AddStep( + label: "CreateModelInputTable", + description: """ + Joins preprocessed shuttle and company TypedFrames with parsed review scores using + Spark distributed joins, then materializes the result to Parquet. + """, + transform: CreateModelInputTableStep.Create(), + input: (catalog.PreprocessedShuttles, catalog.PreprocessedCompanies, catalog.ParsedReviews), + output: catalog.ModelInputTable + ); + }); + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/CreateModelInputTableStep.cs b/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/CreateModelInputTableStep.cs new file mode 100644 index 00000000..00f11013 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/CreateModelInputTableStep.cs @@ -0,0 +1,82 @@ +using Flowthru.Core.Steps; +using Flowthru.Misc.DataFrames; +using KedroSpaceflightsSpark.Data._02_Intermediate.Schemas; +using KedroSpaceflightsSpark.Data._03_Primary.Schemas; + +namespace KedroSpaceflightsSpark.Flows.DataProcessing.Steps; + +/// +/// Joins the three preprocessed TypedFrames using Spark distributed joins to produce +/// the unified model input table. +/// +/// Join order: +/// 1. Shuttles ⋈ ParsedReviews (on shuttle.Id = review.ShuttleId) +/// 2. Result ⋈ Companies (on shuttleReview.CompanyId = company.Id) +/// +/// The result is a TypedFrame<ModelInputTableSchema>. The output catalog item is +/// IItem<IEnumerable<ModelInputTableSchema>> backed by Parquet. Materialization +/// (TypedFrame → IEnumerable) happens implicitly when the Parquet serializer enumerates +/// the frame — no explicit Collect() call required. +/// +[FlowthruStep] +public static class CreateModelInputTableStep +{ + public static Func< + ( + TypedFrame, + TypedFrame, + TypedFrame + ), + TypedFrame + > Create() + { + return (input) => + { + var (shuttles, companies, reviews) = input; + + // Step 1: Join shuttles with parsed reviews on shuttle.Id = review.ShuttleId + var shuttlesWithReviews = shuttles.Join( + reviews, + s => s.Id, + r => r.ShuttleId, + (s, r) => + new ShuttleReviewSchema + { + ShuttleId = s.Id, + ShuttleType = s.ShuttleType, + CompanyId = s.CompanyId, + Engines = s.Engines, + PassengerCapacity = s.PassengerCapacity, + Crew = s.Crew, + Price = s.Price, + DCheckComplete = s.DCheckComplete, + MoonClearanceComplete = s.MoonClearanceComplete, + ReviewScoresRating = r.ReviewScoresRating, + } + ); + + // Step 2: Join with companies on shuttleReview.CompanyId = company.Id + return shuttlesWithReviews.Join( + companies, + sr => sr.CompanyId, + c => c.Id, + (sr, c) => + new ModelInputTableSchema + { + ShuttleId = sr.ShuttleId, + ShuttleType = sr.ShuttleType, + CompanyId = sr.CompanyId, + Engines = sr.Engines, + PassengerCapacity = sr.PassengerCapacity, + Crew = sr.Crew, + DCheckComplete = sr.DCheckComplete, + MoonClearanceComplete = sr.MoonClearanceComplete, + Price = sr.Price, + IataApproved = c.IataApproved, + CompanyRating = c.CompanyRating, + ReviewScoresRating = sr.ReviewScoresRating, + } + ); + }; + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/PreprocessCompaniesStep.cs b/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/PreprocessCompaniesStep.cs new file mode 100644 index 00000000..e6dbea3b --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/PreprocessCompaniesStep.cs @@ -0,0 +1,62 @@ +using Flowthru.Core.Steps; +using Flowthru.Extensions.Spark; +using Flowthru.Misc.DataFrames; +using KedroSpaceflightsSpark.Data._01_Raw.Schemas; +using KedroSpaceflightsSpark.Data._02_Intermediate.Schemas; + +namespace KedroSpaceflightsSpark.Flows.DataProcessing.Steps; + +/// +/// Parses raw company strings into a typed Spark DataFrame. +/// The parsing (percentage strings, boolean flags) runs in C# before the rows are +/// pushed into Spark, keeping Spark focused on the distributed join and aggregation work +/// rather than format-specific cleaning. +/// +[FlowthruStep] +public static class PreprocessCompaniesStep +{ + public static Func, TypedFrame> Create( + SparkFrameProvider frameProvider + ) + { + return (input) => + { + var parsed = input + .Select(Parse) + .Where(item => item != null) + .Cast(); + + return frameProvider.CreateFromEnumerable(parsed); + }; + } + + private static PreprocessedCompanySchema? Parse(CompanySchema raw) + { + bool iataApproved = raw.IataApproved.Trim().ToLowerInvariant() == "t"; + + if (!TryParsePercentage(raw.CompanyRating, out var rating)) + return null; + + return new PreprocessedCompanySchema + { + Id = raw.Id, + CompanyRating = rating, + IataApproved = iataApproved, + CompanyLocation = raw.CompanyLocation, + }; + } + + private static bool TryParsePercentage(string value, out double result) + { + result = 0; + if (string.IsNullOrWhiteSpace(value)) + return false; + + var cleaned = value.Replace("%", "").Trim(); + if (!double.TryParse(cleaned, out var parsed)) + return false; + + result = parsed / 100.0; + return true; + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/PreprocessReviewsStep.cs b/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/PreprocessReviewsStep.cs new file mode 100644 index 00000000..760f9266 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/PreprocessReviewsStep.cs @@ -0,0 +1,35 @@ +using Flowthru.Core.Steps; +using Flowthru.Extensions.Spark; +using Flowthru.Misc.DataFrames; +using KedroSpaceflightsSpark.Data._01_Raw.Schemas; +using KedroSpaceflightsSpark.Data._02_Intermediate.Schemas; + +namespace KedroSpaceflightsSpark.Flows.DataProcessing.Steps; + +/// +/// Filters raw review strings to entries with parseable numeric scores and loads +/// them into a typed Spark DataFrame. +/// +[FlowthruStep] +public static class PreprocessReviewsStep +{ + public static Func, TypedFrame> Create( + SparkFrameProvider frameProvider + ) + { + return (input) => + { + var parsed = input.Select(Parse).Where(r => r != null).Cast(); + + return frameProvider.CreateFromEnumerable(parsed); + }; + } + + private static ParsedReviewSchema? Parse(ReviewSchema raw) + { + if (!double.TryParse(raw.ReviewScoresRating, out var score)) + return null; + + return new ParsedReviewSchema { ShuttleId = raw.ShuttleId, ReviewScoresRating = score }; + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/PreprocessShuttlesStep.cs b/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/PreprocessShuttlesStep.cs new file mode 100644 index 00000000..7250aa47 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/DataProcessing/Steps/PreprocessShuttlesStep.cs @@ -0,0 +1,70 @@ +using Flowthru.Core.Steps; +using Flowthru.Extensions.Spark; +using Flowthru.Misc.DataFrames; +using KedroSpaceflightsSpark.Data._01_Raw.Schemas; +using KedroSpaceflightsSpark.Data._02_Intermediate.Schemas; + +namespace KedroSpaceflightsSpark.Flows.DataProcessing.Steps; + +/// +/// Parses raw shuttle strings into a typed Spark DataFrame. +/// +[FlowthruStep] +public static class PreprocessShuttlesStep +{ + public static Func, TypedFrame> Create( + SparkFrameProvider frameProvider + ) + { + return (input) => + { + var parsed = input + .Select(Parse) + .Where(item => item != null) + .Cast(); + + return frameProvider.CreateFromEnumerable(parsed); + }; + } + + private static PreprocessedShuttleSchema? Parse(ShuttleSchema raw) + { + bool dCheckComplete = raw.DCheckComplete.Trim().ToLowerInvariant() == "t"; + bool moonClearanceComplete = raw.MoonClearanceComplete.Trim().ToLowerInvariant() == "t"; + + if (!int.TryParse(raw.Engines, out var engines)) + return null; + + if (!int.TryParse(raw.PassengerCapacity, out var passengerCapacity)) + return null; + + if (!int.TryParse(raw.Crew, out var crew)) + return null; + + if (!TryParseMoney(raw.Price, out var price)) + return null; + + return new PreprocessedShuttleSchema + { + Id = raw.Id, + ShuttleType = raw.ShuttleType, + CompanyId = raw.CompanyId, + Engines = engines, + PassengerCapacity = passengerCapacity, + Crew = crew, + Price = price, + DCheckComplete = dCheckComplete, + MoonClearanceComplete = moonClearanceComplete, + }; + } + + private static bool TryParseMoney(string value, out double result) + { + result = 0; + if (string.IsNullOrWhiteSpace(value)) + return false; + + var cleaned = value.Replace("$", "").Replace(",", "").Trim(); + return double.TryParse(cleaned, out result); + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/DataScienceFlow.cs b/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/DataScienceFlow.cs new file mode 100644 index 00000000..515ea078 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/DataScienceFlow.cs @@ -0,0 +1,43 @@ +using Flowthru.Core.Flows; +using KedroSpaceflightsSpark.Data; +using KedroSpaceflightsSpark.Flows.DataScience.Steps; + +namespace KedroSpaceflightsSpark.Flows.DataScience; + +public static class DataScienceFlow +{ + public record Params + { + public SplitDataStep.ModelOptions ModelOptions { get; init; } = new(); + } + + public static Flow Create(Catalog catalog, Params parameters) + { + return FlowBuilder.CreateFlow(pipeline => + { + pipeline.AddStep( + label: "SplitData", + description: "Splits model input data into training and test sets.", + transform: SplitDataStep.Create(parameters.ModelOptions), + input: catalog.ModelInputTable, + output: (catalog.TrainSplit, catalog.TestSplit) + ); + + pipeline.AddStep( + label: "TrainModel", + description: "Trains a regression model to predict shuttle prices.", + transform: TrainModelStep.Create(), + input: catalog.TrainSplit, + output: catalog.Regressor + ); + + pipeline.AddStep( + label: "EvaluateModel", + description: "Evaluates the trained model on the test set.", + transform: EvaluateModelStep.Create(), + input: (catalog.Regressor, catalog.TestSplit), + output: (catalog.ModelMetrics, catalog.ModelPredictions) + ); + }); + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/Steps/EvaluateModelStep.cs b/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/Steps/EvaluateModelStep.cs new file mode 100644 index 00000000..34ec30cf --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/Steps/EvaluateModelStep.cs @@ -0,0 +1,79 @@ +using Flowthru.Core.Steps; +using KedroSpaceflightsSpark.Data._05_ModelInput.Schemas; +using KedroSpaceflightsSpark.Data._06_Models.Schemas; +using KedroSpaceflightsSpark.Data._07_ModelOutput.Schemas; + +namespace KedroSpaceflightsSpark.Flows.DataScience.Steps; + +[FlowthruStep] +public static class EvaluateModelStep +{ + public static Func< + (LinearRegressionModel, IEnumerable), + (ModelMetrics, IEnumerable) + > Create() + { + return (input) => + { + var (model, testData) = input; + var data = testData.ToList(); + + if (data.Count == 0) + { + return ( + new ModelMetrics { R2Score = 0, MeanAbsoluteError = 0, MaxError = 0 }, + Enumerable.Empty() + ); + } + + var predictions = data.Select(d => Predict(model, d.Features)).ToList(); + var actuals = data.Select(d => d.Label).ToList(); + + var predictionPairs = actuals + .Zip(predictions, (actual, predicted) => new ModelPredictions { Actual = actual, Predicted = predicted }) + .ToList(); + + return ( + new ModelMetrics + { + R2Score = (decimal)CalculateR2(actuals, predictions), + MeanAbsoluteError = (decimal)CalculateMae(actuals, predictions), + MaxError = (decimal)CalculateMaxError(actuals, predictions), + }, + predictionPairs + ); + }; + } + + private static double Predict(LinearRegressionModel model, FeatureVector features) + { + double prediction = model.Intercept; + double[] featureValues = + [ + features.Engines, + features.PassengerCapacity, + features.Crew, + features.DCheckComplete ? 1.0 : 0.0, + features.IataApproved ? 1.0 : 0.0, + features.CompanyRating, + features.ReviewScoresRating, + ]; + for (int i = 0; i < model.Coefficients.Length; i++) + prediction += model.Coefficients[i] * featureValues[i]; + return prediction; + } + + private static double CalculateR2(List actuals, List predictions) + { + var mean = actuals.Average(); + var ssTotal = actuals.Sum(y => Math.Pow(y - mean, 2)); + var ssResidual = actuals.Zip(predictions, (a, p) => Math.Pow(a - p, 2)).Sum(); + return 1 - (ssResidual / ssTotal); + } + + private static double CalculateMae(List actuals, List predictions) => + actuals.Zip(predictions, (a, p) => Math.Abs(a - p)).Average(); + + private static double CalculateMaxError(List actuals, List predictions) => + actuals.Zip(predictions, (a, p) => Math.Abs(a - p)).Max(); +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/Steps/SplitDataStep.cs b/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/Steps/SplitDataStep.cs new file mode 100644 index 00000000..b858b7bd --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/Steps/SplitDataStep.cs @@ -0,0 +1,68 @@ +using Flowthru.Core.Steps; +using KedroSpaceflightsSpark.Data._03_Primary.Schemas; +using KedroSpaceflightsSpark.Data._05_ModelInput.Schemas; + +namespace KedroSpaceflightsSpark.Flows.DataScience.Steps; + +[FlowthruStep] +public static class SplitDataStep +{ + public record ModelOptions + { + public double TestSize { get; init; } = 0.2; + public int RandomState { get; init; } = 3; + public string[] Features { get; init; } = Array.Empty(); + } + + public static Func< + IEnumerable, + (IEnumerable, IEnumerable) + > Create(ModelOptions options) + { + return (input) => + { + var data = input.ToList(); + var random = new Random(options.RandomState); + var shuffled = data.OrderBy(_ => random.Next()).ToList(); + var splitIndex = (int)(shuffled.Count * (1 - options.TestSize)); + + var trainData = shuffled + .Take(splitIndex) + .Select(row => new TrainingData + { + Features = new FeatureVector + { + Engines = row.Engines, + PassengerCapacity = row.PassengerCapacity, + Crew = row.Crew, + DCheckComplete = row.DCheckComplete, + MoonClearanceComplete = row.MoonClearanceComplete, + IataApproved = row.IataApproved, + CompanyRating = row.CompanyRating, + ReviewScoresRating = row.ReviewScoresRating, + }, + Label = row.Price, + }); + + var testData = shuffled + .Skip(splitIndex) + .Select(row => new TestData + { + Features = new FeatureVector + { + Engines = row.Engines, + PassengerCapacity = row.PassengerCapacity, + Crew = row.Crew, + DCheckComplete = row.DCheckComplete, + MoonClearanceComplete = row.MoonClearanceComplete, + IataApproved = row.IataApproved, + CompanyRating = row.CompanyRating, + ReviewScoresRating = row.ReviewScoresRating, + }, + Label = row.Price, + }); + + return (trainData, testData); + }; + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/Steps/TrainModelStep.cs b/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/Steps/TrainModelStep.cs new file mode 100644 index 00000000..cff74f73 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/DataScience/Steps/TrainModelStep.cs @@ -0,0 +1,57 @@ +using Flowthru.Core.Steps; +using KedroSpaceflightsSpark.Data._05_ModelInput.Schemas; +using KedroSpaceflightsSpark.Data._06_Models.Schemas; +using MathNet.Numerics.LinearRegression; + +namespace KedroSpaceflightsSpark.Flows.DataScience.Steps; + +[FlowthruStep] +public static class TrainModelStep +{ + public static Func, LinearRegressionModel> Create() + { + return (input) => + { + var data = input.ToList(); + + if (data.Count == 0) + throw new InvalidOperationException("No training data available"); + + var features = data.Select(d => d.Features).ToList(); + var labels = data.Select(d => d.Label).ToArray(); + + var featureMatrix = new double[features.Count][]; + for (int i = 0; i < features.Count; i++) + { + featureMatrix[i] = + [ + features[i].Engines, + features[i].PassengerCapacity, + features[i].Crew, + features[i].DCheckComplete ? 1.0 : 0.0, + features[i].IataApproved ? 1.0 : 0.0, + features[i].CompanyRating, + features[i].ReviewScoresRating, + ]; + } + + var coefficients = MultipleRegression.QR(featureMatrix, labels, intercept: true); + + return new LinearRegressionModel + { + Intercept = coefficients[0], + Coefficients = coefficients.Skip(1).ToArray(), + FeatureNames = + [ + "engines", + "passenger_capacity", + "crew", + "d_check_complete", + "iata_approved", + "company_rating", + "review_scores_rating", + ], + }; + }; + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/ReportingFlow.cs b/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/ReportingFlow.cs new file mode 100644 index 00000000..992028cd --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/ReportingFlow.cs @@ -0,0 +1,43 @@ +using Flowthru.Core.Flows; +using KedroSpaceflightsSpark.Data; +using KedroSpaceflightsSpark.Flows.Reporting.Steps; + +namespace KedroSpaceflightsSpark.Flows.Reporting; + +public static class ReportingFlow +{ + public record Params + { + public CreateConfusionMatrixStep.Options ConfusionMatrixOptions { get; init; } = new(); + } + + public static Flow Create(Catalog catalog, Params? parameters = null) + { + var p = parameters ?? new Params(); + + return FlowBuilder.CreateFlow(pipeline => + { + pipeline.AddStep( + label: "ComparePassengerCapacity", + description: "Aggregates average passenger capacity by shuttle type using Spark GroupBy.", + transform: ComparePassengerCapacityStep.Create(), + input: catalog.PreprocessedShuttles, + output: catalog.ShuttleCapacityReport + ); + + pipeline.AddStep( + label: "GeneratePassengerCapacityChart", + transform: GeneratePassengerCapacityChartStep.Create(), + input: catalog.PreprocessedShuttles, + output: catalog.ShuttlePassengerCapacityChart + ); + + pipeline.AddStep( + label: "GenerateConfusionMatrixChart", + transform: CreateConfusionMatrixStep.Create(p.ConfusionMatrixOptions), + input: catalog.ModelPredictions, + output: catalog.ConfusionMatrixChart + ); + }); + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/Steps/ComparePassengerCapacityStep.cs b/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/Steps/ComparePassengerCapacityStep.cs new file mode 100644 index 00000000..85a7da13 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/Steps/ComparePassengerCapacityStep.cs @@ -0,0 +1,34 @@ +using Flowthru.Core.Steps; +using Flowthru.Misc.DataFrames; +using KedroSpaceflightsSpark.Data._02_Intermediate.Schemas; +using KedroSpaceflightsSpark.Data._08_Reporting.Schemas; + +namespace KedroSpaceflightsSpark.Flows.Reporting.Steps; + +/// +/// Aggregates average passenger capacity by shuttle type using a Spark GroupBy. +/// +/// The input is a TypedFrame. The step chains GroupBy.Aggregate on the distributed frame, +/// then calls ToList() to materialize the result. Materialization triggers the Spark action +/// and hydrates the collected rows into typed records. +/// +[FlowthruStep] +public static class ComparePassengerCapacityStep +{ + public static Func< + TypedFrame, + IEnumerable + > Create() + { + return (input) => + input + .GroupBy(s => s.ShuttleType) + .Aggregate(ctx => new ShuttleCapacityReport + { + ShuttleType = ctx.Key, + AvgPassengerCapacity = ctx.Avg(s => (double)s.PassengerCapacity), + }) + .OrderBy(r => r.ShuttleType) + .ToList(); + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/Steps/CreateConfusionMatrixStep.cs b/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/Steps/CreateConfusionMatrixStep.cs new file mode 100644 index 00000000..89d0ab8f --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/Steps/CreateConfusionMatrixStep.cs @@ -0,0 +1,90 @@ +using Flowthru.Core.Steps; +using KedroSpaceflightsSpark.Data._07_ModelOutput.Schemas; +using Microsoft.Extensions.Logging; +using Plotly.NET; +using Plotly.NET.LayoutObjects; +using CSharpChart = Plotly.NET.CSharp.Chart; + +namespace KedroSpaceflightsSpark.Flows.Reporting.Steps; + +[FlowthruStep] +public static class CreateConfusionMatrixStep +{ + public record Options + { + public int NumBins { get; init; } = 4; + } + + public static Func, GenericChart> Create( + Options? options = null, + ILogger? logger = null + ) + { + var opts = options ?? new Options(); + + return input => + { + var predictions = input.ToList(); + + if (!predictions.Any()) + throw new InvalidOperationException("Cannot create confusion matrix from empty predictions"); + + logger?.LogInformation( + "Generating confusion matrix from {Count} predictions using {NumBins} bins", + predictions.Count, + opts.NumBins + ); + + var sortedActuals = predictions.Select(p => p.Actual).OrderBy(v => v).ToList(); + var thresholds = CalculatePercentileThresholds(sortedActuals, opts.NumBins); + + var binnedPredictions = predictions + .Select(p => (Actual: AssignBin(p.Actual, thresholds), Predicted: AssignBin(p.Predicted, thresholds))) + .ToList(); + + var matrix = new int[opts.NumBins, opts.NumBins]; + foreach (var (actual, predicted) in binnedPredictions) + matrix[actual, predicted]++; + + var zValues = new List>(); + for (int i = 0; i < opts.NumBins; i++) + { + var row = new List(); + for (int j = 0; j < opts.NumBins; j++) + row.Add(matrix[i, j]); + zValues.Add(row); + } + + var binLabels = Enumerable.Range(1, opts.NumBins).Select(i => $"Q{i}").ToList(); + + return CSharpChart + .Heatmap(zValues, X: binLabels, Y: binLabels, ShowScale: true) + .WithXAxisStyle(Title.init("Predicted")) + .WithYAxisStyle(Title.init("Actual")) + .WithTitle($"Prediction Confusion Matrix ({opts.NumBins} bins)") + .WithSize(Math.Max(600, opts.NumBins * 80), Math.Max(600, opts.NumBins * 80)); + }; + } + + private static List CalculatePercentileThresholds(List sortedValues, int numBins) + { + var thresholds = new List(); + for (int i = 1; i < numBins; i++) + { + var index = (int)Math.Round((double)i / numBins * sortedValues.Count) - 1; + index = Math.Max(0, Math.Min(index, sortedValues.Count - 1)); + thresholds.Add(sortedValues[index]); + } + return thresholds; + } + + private static int AssignBin(double value, List thresholds) + { + for (int i = 0; i < thresholds.Count; i++) + { + if (value <= thresholds[i]) + return i; + } + return thresholds.Count; + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/Steps/GeneratePassengerCapacityChartStep.cs b/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/Steps/GeneratePassengerCapacityChartStep.cs new file mode 100644 index 00000000..27ce2c37 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Flows/Reporting/Steps/GeneratePassengerCapacityChartStep.cs @@ -0,0 +1,53 @@ +using Flowthru.Core.Steps; +using Flowthru.Misc.DataFrames; +using KedroSpaceflightsSpark.Data._02_Intermediate.Schemas; +using Microsoft.Extensions.Logging; +using Plotly.NET; +using Plotly.NET.LayoutObjects; +using CSharpChart = Plotly.NET.CSharp.Chart; + +namespace KedroSpaceflightsSpark.Flows.Reporting.Steps; + +/// +/// Generates a bar chart comparing average passenger capacity by shuttle type. +/// Receives a TypedFrame and enumerates it (triggering Spark materialization) to +/// produce the aggregated data needed by Plotly.NET. +/// +[FlowthruStep] +public static class GeneratePassengerCapacityChartStep +{ + public static Func, GenericChart> Create( + ILogger? logger = null + ) + { + return (input) => + { + var shuttles = input.ToList(); + + logger?.LogInformation( + "Generating passenger capacity chart from {Count} shuttle records", + shuttles.Count + ); + + var aggregated = shuttles + .GroupBy(s => s.ShuttleType) + .Select(g => new + { + ShuttleType = g.Key, + AvgPassengerCapacity = g.Average(s => s.PassengerCapacity), + }) + .OrderByDescending(x => x.AvgPassengerCapacity) + .ToList(); + + var shuttleTypes = aggregated.Select(x => x.ShuttleType).ToList(); + var capacities = aggregated.Select(x => x.AvgPassengerCapacity).ToList(); + + return CSharpChart + .Column(shuttleTypes, capacities) + .WithXAxisStyle(Title.init("Shuttle Type (Ranked by Capacity)")) + .WithYAxisStyle(Title.init("Average Passenger Capacity")) + .WithTitle("Shuttle Passenger Capacity Rankings") + .WithSize(1000, 600); + }; + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/KedroSpaceflightsSpark.csproj b/examples/starter/KedroSpaceflightsSpark/KedroSpaceflightsSpark.csproj new file mode 100644 index 00000000..ed649850 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/KedroSpaceflightsSpark.csproj @@ -0,0 +1,28 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + PreserveNewest + + + + + + + + + + + diff --git a/examples/starter/KedroSpaceflightsSpark/Program.cs b/examples/starter/KedroSpaceflightsSpark/Program.cs new file mode 100644 index 00000000..fda5eccb --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/Program.cs @@ -0,0 +1,82 @@ +using Flowthru.Core.Cli; +using Flowthru.Core.Services; +using Flowthru.Extensions.Spark; +using Flowthru.Extensions.Spark.Services; +using Flowthru.Meta; +using Flowthru.Meta.Providers; +using KedroSpaceflightsSpark.Data; +using KedroSpaceflightsSpark.Flows.DataProcessing; +using KedroSpaceflightsSpark.Flows.DataScience; +using KedroSpaceflightsSpark.Flows.Reporting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace KedroSpaceflightsSpark; + +public class Program +{ + public static Task Main(string[] args) => + FlowthruCli.RunStandaloneAsync( + args, + services => ConfigureServices(services, Directory.GetCurrentDirectory()) + ); + + public static IServiceProvider ConfigureServices(string? basePath = null) + { + var services = new ServiceCollection(); + ConfigureServices(services, basePath ?? Directory.GetCurrentDirectory()); + return services.BuildServiceProvider(); + } + + private static void ConfigureServices(IServiceCollection services, string basePath) + { + services.AddFlowthru(flowthru => + { + flowthru.UseConfiguration(opts => opts.ConfigurationPath = basePath); + + flowthru.RegisterCatalog(sp => new Catalog( + Path.Combine(basePath, "Data"), + sp.GetRequiredService() + )); + + flowthru.ConfigureMetadata(meta => + { + var metadataPath = Path.Combine(basePath, "Metadata"); + meta.AddProvider(json => + json.WithOutputDirectory(metadataPath) + ) + .AddProvider(mermaid => + mermaid.WithOutputDirectory(metadataPath) + ); + }); + + flowthru + .RegisterFlow(label: "DataProcessing", flow: DataProcessingFlow.Create) + .WithDescription("Preprocesses companies, shuttles, and reviews data using Spark DataFrames"); + + flowthru + .RegisterFlow( + label: "DataScience", + flow: DataScienceFlow.Create, + configurationSection: "Flowthru:Flows:DataScience" + ) + .WithDescription("Trains linear regression model for price prediction"); + + flowthru + .RegisterFlow( + label: "Reporting", + flow: ReportingFlow.Create, + configurationSection: "Flowthru:Flows:Reporting" + ) + .WithDescription("Generates passenger capacity reports and visualizations"); + + flowthru.UseSpark(); + }); + + services.AddLogging(logging => + { + logging.AddConsole(); + logging.SetMinimumLevel(LogLevel.Information); + }); + } +} diff --git a/examples/starter/KedroSpaceflightsSpark/appsettings.json b/examples/starter/KedroSpaceflightsSpark/appsettings.json new file mode 100644 index 00000000..0ba7b7b0 --- /dev/null +++ b/examples/starter/KedroSpaceflightsSpark/appsettings.json @@ -0,0 +1,33 @@ +{ + "Flowthru": { + "Flows": { + "DataScience": { + "ModelOptions": { + "TestSize": 0.2, + "RandomState": 3, + "Features": [ + "engines", + "passenger_capacity", + "crew", + "d_check_complete", + "iata_approved", + "company_rating", + "review_scores_rating" + ] + } + }, + "Reporting": { + "ConfusionMatrixOptions": { + "NumBins": 5 + } + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Flowthru": "Debug", + "Microsoft": "Warning" + } + } + } +} diff --git a/examples/starter/project.json b/examples/starter/project.json index f438f4f5..611371ff 100644 --- a/examples/starter/project.json +++ b/examples/starter/project.json @@ -8,7 +8,7 @@ "//": "Packs all packable projects in the workspace. src/ lands in dist/packages/ (flat); others go under dist/packages//", "executor": "nx:run-commands", "options": { - "commands": ["pnpm nx run-many -t pack"], + "commands": [ "pnpm nx run-many -t pack" ], "cwd": "{workspaceRoot}", "parallel": false } @@ -17,7 +17,7 @@ "//": "Stamps the current Directory.Build.props version into all starter template.json files", "executor": "nx:run-commands", "options": { - "commands": ["bash scripts/inject-template-version.sh"], + "commands": [ "bash scripts/inject-template-version.sh" ], "cwd": "{workspaceRoot}", "parallel": false } @@ -31,6 +31,7 @@ "cp docs/guides/misc/AGENTS.md examples/starter/KedroIrisPython/AGENTS.md", "cp docs/guides/misc/AGENTS.md examples/starter/KedroSpaceflights/AGENTS.md", "cp docs/guides/misc/AGENTS.md examples/starter/KedroSpaceflightsPython/AGENTS.md", + "cp docs/guides/misc/AGENTS.md examples/starter/KedroSpaceflightsSpark/AGENTS.md", "cp docs/guides/misc/AGENTS.md examples/starter/Minimal/AGENTS.md", "cp docs/guides/misc/AGENTS.md examples/starter/SpaceflightsEFCore/AGENTS.md" ], diff --git a/global.json b/global.json new file mode 100644 index 00000000..9eeb4288 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.101", + "rollForward": "latestFeature" + } +} diff --git a/package.json b/package.json index 2d7ab1f0..1ae7b1c9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A type-safe data engineering framework for .NET", "main": "index.js", "scripts": { - "postinstall": "dotnet restore", + "postinstall": "bash scripts/post-pnpm-install-hook.sh", "prepare": "husky" }, "keywords": [ diff --git a/project.json b/project.json index 5d186f67..e2b85152 100644 --- a/project.json +++ b/project.json @@ -4,39 +4,6 @@ "projectType": "library", "sourceRoot": ".", "targets": { - "coverage:badges": { - "//": "Generate coverage badges from the most recent test run results", - "executor": "nx:run-commands", - "dependsOn": [ ], - "options": { - "commands": [ - "reportgenerator \"-reports:tests/**/TestResults/**/coverage.cobertura.xml\" \"-targetdir:coverage-badges\" \"-reporttypes:Badges\" \"-verbosity:Warning\"" - ], - "parallel": false - } - }, - "coverage:report": { - "//": "Generate HTML coverage report from the most recent test run results", - "executor": "nx:run-commands", - "dependsOn": [ ], - "options": { - "commands": [ - "reportgenerator \"-reports:tests/**/TestResults/**/coverage.cobertura.xml\" \"-targetdir:CoverageReport\" \"-reporttypes:Html;Badges\" \"-verbosity:Info\"" - ], - "parallel": false - } - }, - "ci": { - "//": "Full local CI: run tests and generate HTML coverage report", - "executor": "nx:run-commands", - "dependsOn": [ ], - "options": { - "commands": [ - "echo '✓ CI pipeline complete'" - ], - "parallel": false - } - }, "release": { "//": "Determine next version via NX Release, sync to Directory.Build.props, generate changelog, create GitHub Release", "executor": "nx:run-commands", @@ -48,45 +15,14 @@ } }, "format": { - "//": "Applies all formatting for the project", + "//": "Format all C# source files with CSharpier", "executor": "nx:run-commands", "options": { - "commands": [ ], - "cwd": "project.json", - "parallel": false - }, - "configurations": { - "csharp": { - "options": { - "commands": [ - "dotnet csharpier ." - ], - "cwd": ".", - "parallel": false - } - } - } - }, - "purge": { - "//": "Purges non-essential files and folders", - "executor": "nx:run-commands", - "options": { - "commands": [ ], - "cwd": "", + "commands": [ + "dotnet csharpier ." + ], + "cwd": ".", "parallel": false - }, - "configurations": { - "coverage": { - "//": "Purges previous coverage reports", - "options": { - "commands": [ - "echo 'Purging previous coverage data...'", - "find tests -name TestResults -type d -exec rm -rf {} + 2>/dev/null || true", - "rm -rf CoverageReport" - ], - "parallel": false - } - } } } } diff --git a/scripts/post-pnpm-install-hook.sh b/scripts/post-pnpm-install-hook.sh new file mode 100755 index 00000000..5bc957e9 --- /dev/null +++ b/scripts/post-pnpm-install-hook.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Post-pnpm-install hook. +# Checks for required non-Node dependencies and runs any associated setup actions. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +ok() { echo " [OK] $*"; } +warn() { echo " [!!] $*" >&2; } + +echo "" +echo "Checking non-Node dependencies..." +echo "" + +# ── dotnet ──────────────────────────────────────────────────────────────────── +# Required to build and test all Flowthru projects. +# On success, runs 'dotnet restore' to prime the NuGet cache. + +REQUIRED_DOTNET_MAJOR=$(grep -o '"version":[[:space:]]*"[^"]*"' "$ROOT_DIR/global.json" \ + | grep -o '[0-9][0-9]*' | head -1) + +if command -v dotnet &>/dev/null; then + DOTNET_VERSION=$(dotnet --version 2>/dev/null) + DOTNET_MAJOR=$(echo "$DOTNET_VERSION" | cut -d. -f1) + + if [ "$DOTNET_MAJOR" -ge "$REQUIRED_DOTNET_MAJOR" ]; then + ok "dotnet $DOTNET_VERSION (global.json requires .NET $REQUIRED_DOTNET_MAJOR+)" + echo "" + echo " Running dotnet restore..." + dotnet restore "$ROOT_DIR" + ok "dotnet restore complete" + else + warn "dotnet $DOTNET_VERSION found, but .NET $REQUIRED_DOTNET_MAJOR+ is required (see global.json)." + warn "Download: https://dotnet.microsoft.com/download" + fi +else + warn "dotnet not found. .NET $REQUIRED_DOTNET_MAJOR+ is required to build and test Flowthru." + warn "Download: https://dotnet.microsoft.com/download" +fi + +echo "" + +# ── uv ──────────────────────────────────────────────────────────────────────── +# Required for Python-backed flows and the Flowthru.Extensions.Python test suite. + +if command -v uv &>/dev/null; then + UV_VERSION=$(uv --version 2>/dev/null | awk '{print $2}') + ok "uv $UV_VERSION" +else + warn "uv not found. uv is required for Python-backed Flowthru flows and tests." + warn "Install: https://docs.astral.sh/uv/getting-started/installation/" +fi + +echo "" + +# ── python ──────────────────────────────────────────────────────────────────── +# Required for the Flowthru.Extensions.Python extension and example projects. + +if command -v python3 &>/dev/null; then + PYTHON_VERSION=$(python3 --version 2>/dev/null | awk '{print $2}') + ok "python $PYTHON_VERSION" +elif command -v python &>/dev/null; then + PYTHON_VERSION=$(python --version 2>/dev/null | awk '{print $2}') + ok "python $PYTHON_VERSION" +else + warn "python not found. Python 3.10+ is required for the Flowthru Python extension and examples." + warn "Install via uv: https://docs.astral.sh/uv/ or https://www.python.org/downloads/" +fi + +echo "" + +# ── java ────────────────────────────────────────────────────────────────────── +# Required to run Spark-backed tests. Spark 4.1.1 requires JDK 17+. + +if command -v java &>/dev/null; then + JAVA_VERSION=$(java -version 2>&1 | head -1) + ok "java: $JAVA_VERSION" +else + warn "java not found. JDK 17+ is required to run Spark-backed Flowthru tests." +fi + +echo "" + +# ── SPARK_HOME ──────────────────────────────────────────────────────────────── +# Required to run Spark-backed tests (Flowthru.Extensions.Spark.Tests). +# Tests guard themselves with Assume checks and skip gracefully if unset. + +if [ -n "${SPARK_HOME:-}" ] && [ -d "$SPARK_HOME" ]; then + if [ -f "$SPARK_HOME/RELEASE" ]; then + SPARK_RELEASE=$(head -1 "$SPARK_HOME/RELEASE") + ok "SPARK_HOME=$SPARK_HOME ($SPARK_RELEASE)" + else + ok "SPARK_HOME=$SPARK_HOME" + fi +else + warn "SPARK_HOME is not set or does not point to a valid directory." + warn "Apache Spark 4.1.1 is required to run Spark-backed Flowthru tests." + warn "Download: https://spark.apache.org/downloads.html" +fi + +echo "" diff --git a/src/core/Flowthru.Core/Services/FlowthruServiceBuilder.cs b/src/core/Flowthru.Core/Services/FlowthruServiceBuilder.cs index db8855b2..01c8b820 100644 --- a/src/core/Flowthru.Core/Services/FlowthruServiceBuilder.cs +++ b/src/core/Flowthru.Core/Services/FlowthruServiceBuilder.cs @@ -75,6 +75,21 @@ public FlowthruServiceBuilder ConfigureExecution(Action config return this; } + /// + /// Escape hatch for extension packages that need to register additional services + /// with the underlying . + /// + /// Action that receives the service collection. + /// This builder for method chaining. + public FlowthruServiceBuilder ConfigureServices(Action configure) + { + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + + configure(_services); + return this; + } + /// /// Internal entry type that carries a Flow factory and its associated metadata. /// Replaces the FlowRegistrar indirection for cleaner multi-catalog support. diff --git a/src/extensions/Flowthru.Extensions.Spark.CodeFixes/Flowthru.Extensions.Spark.CodeFixes.csproj b/src/extensions/Flowthru.Extensions.Spark.CodeFixes/Flowthru.Extensions.Spark.CodeFixes.csproj new file mode 100644 index 00000000..149de804 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark.CodeFixes/Flowthru.Extensions.Spark.CodeFixes.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + latest + enable + true + Flowthru.Extensions.Spark.CodeFixes + + RS2008;RS2007 + true + false + false + + + + + + + + + + + + diff --git a/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/AnalyzerReleases.Shipped.md b/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..5150bd16 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1,9 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +## Release 1.0 + +### New Rules + +| Rule ID | Category | Severity | Notes | +| ------- | -------- | -------- | ----- | diff --git a/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/AnalyzerReleases.Unshipped.md b/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..88128c6b --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +| Rule ID | Category | Severity | Notes | +| ---------- | -------------- | -------- | --------------------------------------------------------- | +| FSPARK1002 | Flowthru.Spark | Error | Method call in TypedFrame lambda has no Spark translation | diff --git a/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/Flowthru.Extensions.Spark.SourceGenerators.csproj b/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/Flowthru.Extensions.Spark.SourceGenerators.csproj new file mode 100644 index 00000000..cd6c3ee3 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/Flowthru.Extensions.Spark.SourceGenerators.csproj @@ -0,0 +1,38 @@ + + + + netstandard2.0 + latest + enable + true + Flowthru.Extensions.Spark.SourceGenerators + true + RS2008;RS2007 + true + false + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/SparkDiagnostics.cs b/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/SparkDiagnostics.cs new file mode 100644 index 00000000..67bd055f --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/SparkDiagnostics.cs @@ -0,0 +1,31 @@ +using Microsoft.CodeAnalysis; + +namespace Flowthru.Extensions.Spark.Analyzers; + +/// +/// Diagnostic descriptors for the Spark expression analyzer. +/// +public static class SparkDiagnostics +{ + private const string Category = "Flowthru.Spark"; + + /// + /// FSPARK1002: A method call inside a TypedFrame lambda has no translation in the + /// Spark provider. The supported sets are defined in + /// . + /// + public static readonly DiagnosticDescriptor UnsupportedMethodCall = + new( + id: "FSPARK1002", + title: "Method call in TypedFrame lambda has no Spark translation", + messageFormat: "'{0}.{1}' cannot be translated to a Spark Column expression. " + + "Supported string methods: {2}. Supported Math methods: {3}.", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The Spark provider can only translate a specific subset of method calls " + + "inside TypedFrame lambdas. Calls outside this set will fail at runtime. " + + "To add support for a new method, add it to SparkTranslatableOperations and " + + "implement the corresponding switch arm in SparkExpressionVisitor." + ); +} diff --git a/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/SparkExpressionAnalyzer.cs b/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/SparkExpressionAnalyzer.cs new file mode 100644 index 00000000..548e428d --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark.SourceGenerators/SparkExpressionAnalyzer.cs @@ -0,0 +1,117 @@ +using System.Collections.Immutable; +using System.Linq; +using Flowthru.Extensions.Spark.Shared; +using Flowthru.Misc.DataFrames.Analyzers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Flowthru.Extensions.Spark.Analyzers; + +/// +/// Validates that method calls inside TypedFrameExtensions lambdas are within the +/// subset that SparkExpressionVisitor can translate. +/// +/// +/// The translatable subset is defined in +/// , which is the single source of truth shared +/// between this analyzer and the runtime visitor. Adding a method to the visitor without +/// updating SparkTranslatableOperations will be caught by the sync-validation test +/// in Flowthru.Tests.Spark. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class SparkExpressionAnalyzer : DiagnosticAnalyzer +{ + // Pre-formatted strings for the diagnostic message — computed once. + private static readonly string _supportedStringList = string.Join( + ", ", + SparkTranslatableOperations.SupportedStringMethods + ); + private static readonly string _supportedMathList = string.Join( + ", ", + SparkTranslatableOperations.SupportedMathMethods + ); + + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(SparkDiagnostics.UnsupportedMethodCall); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + var invocation = (InvocationExpressionSyntax)context.Node; + var match = TypedFrameInvocationHelper.TryMatch(invocation, context.SemanticModel); + + if (match is null) + return; + + // Check all lambda arguments for unsupported method calls in their bodies. + foreach (var lambda in match.LambdaArguments) + { + foreach (var inner in lambda.DescendantNodes().OfType()) + { + CheckInnerInvocation(context, inner); + } + } + } + + private static void CheckInnerInvocation( + SyntaxNodeAnalysisContext context, + InvocationExpressionSyntax invocation + ) + { + if (context.SemanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method) + return; + + var declaringType = method.ContainingType; + if (declaringType is null) + return; + + // string instance methods: only fire if the declaring type is string and method is + // not in the supported set. + if ( + declaringType.SpecialType == SpecialType.System_String + && !SparkTranslatableOperations.SupportedStringMethods.Contains(method.Name) + ) + { + ReportUnsupported(context, invocation, declaringType.Name, method.Name); + return; + } + + // Math static methods: only fire if declaring type is System.Math and method is not + // in the supported set. + if ( + declaringType.ContainingNamespace?.Name == "System" + && declaringType.Name == "Math" + && !SparkTranslatableOperations.SupportedMathMethods.Contains(method.Name) + ) + { + ReportUnsupported(context, invocation, declaringType.Name, method.Name); + } + } + + private static void ReportUnsupported( + SyntaxNodeAnalysisContext context, + InvocationExpressionSyntax invocation, + string typeName, + string methodName + ) + { + context.ReportDiagnostic( + Diagnostic.Create( + SparkDiagnostics.UnsupportedMethodCall, + invocation.GetLocation(), + typeName, + methodName, + _supportedStringList, + _supportedMathList + ) + ); + } +} diff --git a/src/extensions/Flowthru.Extensions.Spark/Data/FrameItemFactory.cs b/src/extensions/Flowthru.Extensions.Spark/Data/FrameItemFactory.cs new file mode 100644 index 00000000..1ba683fb --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/Data/FrameItemFactory.cs @@ -0,0 +1,48 @@ +using Flowthru.Core.Abstractions; +using Flowthru.Core.Data; +using Flowthru.Core.Data.Storage; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Data; + +/// +/// Extension point for ItemFactory.Frame factory methods. +/// +/// +/// +/// items are always in-memory. There is intentionally no +/// file-backed variant: a represents a deferred Spark execution +/// plan, not a materialized dataset. Persisting or loading a plan from disk is meaningless — +/// use a file-backed ItemFactory.Enumerable.* item at the point where data must be +/// materialized instead. +/// +/// +/// The in-memory storage adapter passes the reference directly +/// between steps (Save → Load is a reference assignment). No Spark action is triggered; the +/// execution plan remains deferred until a step explicitly calls +/// . +/// +/// +public sealed class FrameItemFactory +{ + internal FrameItemFactory() { } + + /// + /// Creates an in-memory catalog item holding a . + /// + /// + /// The schema type for the frame's rows. Must be a flat schema — non-flat types + /// contain nested or collection properties incompatible with scalar Spark columns. + /// + /// Unique catalog label for DAG resolution. + /// + /// A catalog item with memory storage. No serialization occurs; the frame reference + /// is passed between steps as-is. + /// + public Item> Memory(string label) + where TRow : notnull, IFlatSchema + { + var storage = new MemoryStorageAdapter>(); + return new Item>(label, storage); + } +} diff --git a/src/extensions/Flowthru.Extensions.Spark/Data/ItemFactory.Frame.cs b/src/extensions/Flowthru.Extensions.Spark/Data/ItemFactory.Frame.cs new file mode 100644 index 00000000..f75ea5c1 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/Data/ItemFactory.Frame.cs @@ -0,0 +1,19 @@ +using Flowthru.Core.Data; +using Flowthru.Extensions.Spark.Data; + +namespace Flowthru.Extensions.Spark; + +/// +/// Extends with a Frame property for +/// catalog items. +/// +public static partial class ItemFactory +{ + /// + /// Factory methods for catalog entries. + /// + /// + /// TypedFrame items are always in-memory. See for details. + /// + public static FrameItemFactory Frame { get; } = new FrameItemFactory(); +} diff --git a/src/extensions/Flowthru.Extensions.Spark/Flowthru.Extensions.Spark.csproj b/src/extensions/Flowthru.Extensions.Spark/Flowthru.Extensions.Spark.csproj new file mode 100644 index 00000000..6116d455 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/Flowthru.Extensions.Spark.csproj @@ -0,0 +1,37 @@ + + + + net10.0 + enable + enable + Flowthru.Extensions.Spark + Apache Spark integration for Flowthru via typed DataFrame wrappers over Spark.NET + flowthru;spark;apache-spark;dataframe + + + Flowthru.Extensions.Spark + + + + + <_Parameter1>Flowthru.Extensions.Spark.Tests + + + + + + + + + + + + + + + + + + diff --git a/src/extensions/Flowthru.Extensions.Spark/README.md b/src/extensions/Flowthru.Extensions.Spark/README.md new file mode 100644 index 00000000..e69de29b diff --git a/src/extensions/Flowthru.Extensions.Spark/Runtime/MelLoggerService.cs b/src/extensions/Flowthru.Extensions.Spark/Runtime/MelLoggerService.cs new file mode 100644 index 00000000..2145ee92 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/Runtime/MelLoggerService.cs @@ -0,0 +1,71 @@ +using System; +using Flowthru.Spark.Services; +using Microsoft.Extensions.Logging; + +namespace Flowthru.Extensions.Spark.Runtime; + +/// +/// Bridges the Spark library's internal to the +/// standard Microsoft.Extensions.Logging pipeline so that JVM bridge diagnostics +/// respect the application's configured log levels and sinks. +/// +internal sealed class MelLoggerService : ILoggerService +{ + private readonly ILoggerFactory _factory; + private readonly ILogger _logger; + + internal MelLoggerService(ILoggerFactory factory) + : this(factory, factory.CreateLogger("Flowthru.Spark")) { } + + private MelLoggerService(ILoggerFactory factory, ILogger logger) + { + _factory = factory; + _logger = logger; + } + + public bool IsDebugEnabled => _logger.IsEnabled(LogLevel.Debug); + + public ILoggerService GetLoggerInstance(Type type) + { + try + { + return new MelLoggerService(_factory, _factory.CreateLogger(type.FullName ?? type.Name)); + } + catch (ObjectDisposedException) + { + // The ILoggerFactory was disposed before this call — typically during test teardown + // when GC finalizers on JvmObjectId fire after the DI container has been released. + // Return the parent instance as a safe no-op fallback so the JvmObjectId static + // constructor completes cleanly rather than poisoning the type with a + // TypeInitializationException. + return this; + } + } + + public void LogDebug(string message) => _logger.LogDebug("{Message}", message); + + public void LogDebug(string messageFormat, params object[] messageParameters) => + _logger.LogDebug(messageFormat, messageParameters); + + public void LogInfo(string message) => _logger.LogInformation("{Message}", message); + + public void LogInfo(string messageFormat, params object[] messageParameters) => + _logger.LogInformation(messageFormat, messageParameters); + + public void LogWarn(string message) => _logger.LogWarning("{Message}", message); + + public void LogWarn(string messageFormat, params object[] messageParameters) => + _logger.LogWarning(messageFormat, messageParameters); + + public void LogError(string message) => _logger.LogError("{Message}", message); + + public void LogError(string messageFormat, params object[] messageParameters) => + _logger.LogError(messageFormat, messageParameters); + + public void LogFatal(string message) => _logger.LogCritical("{Message}", message); + + public void LogFatal(string messageFormat, params object[] messageParameters) => + _logger.LogCritical(messageFormat, messageParameters); + + public void LogException(Exception e) => _logger.LogError(e, "Exception in Spark JVM bridge"); +} diff --git a/src/extensions/Flowthru.Extensions.Spark/Runtime/SparkRuntime.cs b/src/extensions/Flowthru.Extensions.Spark/Runtime/SparkRuntime.cs new file mode 100644 index 00000000..5c3f1533 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/Runtime/SparkRuntime.cs @@ -0,0 +1,216 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Services; +using Microsoft.Extensions.Logging; + +namespace Flowthru.Extensions.Spark.Runtime; + +/// +/// Manages the lifecycle of the local Spark JVM backend process. +/// +/// +/// +/// On , spawns spark-submit with the Flowthru JVM bridge JAR +/// in debug mode, which starts DotnetBackend listening on a TCP port. +/// connects to that port on +/// its first use — so the backend must be running before any Spark API call. +/// +/// +/// Registered as a singleton. is idempotent — safe to call multiple times. +/// +/// +public sealed class SparkRuntime : IDisposable +{ + private static readonly object _lock = new(); + + private readonly SparkRuntimeOptions _options; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private Process? _backendProcess; + private int _backendPort; + private bool _initialized; + private bool _disposed; + + /// + /// Initializes a new instance of . + /// + public SparkRuntime( + SparkRuntimeOptions options, + ILogger logger, + ILoggerFactory loggerFactory) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + } + + /// + /// Starts the Spark JVM backend process and waits for it to accept connections. + /// + /// + /// Idempotent — subsequent calls return immediately if already initialized. + /// + /// + /// Thrown if the backend does not become ready within the timeout. + /// + /// + /// Thrown if this instance has been disposed. + /// + public void Initialize() + { + if (_initialized) + return; + + if (_disposed) + throw new ObjectDisposedException(nameof(SparkRuntime)); + + lock (_lock) + { + if (_initialized) + return; + + // Route all Spark library internal logs through the MEL pipeline so they + // respect the application's configured log levels and sinks instead of + // writing directly to stdout via ConsoleLoggerService. + LoggerServiceFactory.SetLoggerService(new MelLoggerService(_loggerFactory)); + + var sparkHome = _options.GetResolvedSparkHome(); + var jarPath = _options.GetResolvedJarPath(); + var master = _options.Master; + + _logger.LogInformation("Spark home: {SparkHome}", sparkHome); + _logger.LogInformation("Bridge JAR: {JarPath}", jarPath); + _logger.LogInformation("Master: {Master}", master); + + _backendPort = FindFreePort(); + Environment.SetEnvironmentVariable("DOTNETBACKEND_PORT", _backendPort.ToString()); + _logger.LogInformation("DotnetBackend port: {Port}", _backendPort); + + var sparkSubmit = Path.Combine(sparkHome, "bin", "spark-submit"); + var args = $"--class org.apache.spark.deploy.dotnet.DotnetRunner " + + $"--master {master} " + + $"\"{jarPath}\" debug {_backendPort}"; + + _logger.LogDebug("Launching: {SparkSubmit} {Args}", sparkSubmit, args); + + var psi = new ProcessStartInfo + { + FileName = sparkSubmit, + Arguments = args, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + + // Pass the chosen port to DotnetRunner so it binds to the same port we will poll. + psi.Environment["DOTNETBACKEND_PORT"] = _backendPort.ToString(); + + _backendProcess = new Process { StartInfo = psi, EnableRaisingEvents = true }; + + // Forward JVM output to the .NET logger at debug level + _backendProcess.OutputDataReceived += (_, e) => + { + if (e.Data != null) + _logger.LogDebug("[Spark JVM] {Line}", e.Data); + }; + _backendProcess.ErrorDataReceived += (_, e) => + { + if (e.Data != null) + _logger.LogDebug("[Spark JVM stderr] {Line}", e.Data); + }; + + _backendProcess.Start(); + _backendProcess.BeginOutputReadLine(); + _backendProcess.BeginErrorReadLine(); + + WaitForPort(_backendPort, timeoutSeconds: _options.BackendStartupTimeoutSeconds); + + _initialized = true; + _logger.LogInformation("Spark JVM backend ready on port {Port}", _backendPort); + } + } + + /// + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + + if (_backendProcess is { HasExited: false }) + { + try + { + _logger.LogInformation("Stopping Spark JVM backend (pid {Pid})", _backendProcess.Id); + + // Kill the process directly. In local[*] debug mode the .NET side does not + // hold a SparkContext reference, so stopActiveSparkContext would reliably + // fail. The session teardown responsibility belongs to whoever holds the + // SparkSession (e.g., the user's Catalog). In a future cluster-aware refactor + // SparkRuntime should accept an optional SparkSession and call session.Stop() + // here before killing when running locally, and skip the Kill() entirely + // when connected to an external cluster. + _backendProcess.Kill(entireProcessTree: true); + _backendProcess.WaitForExit(5000); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Exception while stopping Spark JVM backend"); + } + finally + { + _backendProcess.Dispose(); + _backendProcess = null; + } + } + } + + private static int FindFreePort() + { + // Bind on port 0 to let the OS assign a free port, then release it. + // There is a small TOCTOU window, but it's negligible for local dev use. + var envPort = Environment.GetEnvironmentVariable("DOTNETBACKEND_PORT"); + if (!string.IsNullOrWhiteSpace(envPort) && int.TryParse(envPort, out var configured)) + return configured; + + using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0); + listener.Start(); + var port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + private void WaitForPort(int port, int timeoutSeconds) + { + var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds); + while (DateTime.UtcNow < deadline) + { + try + { + using var client = new TcpClient(); + client.Connect("127.0.0.1", port); + return; + } + catch (SocketException) + { + if (_backendProcess is { HasExited: true }) + throw new InvalidOperationException( + $"Spark JVM backend process exited unexpectedly (exit code {_backendProcess.ExitCode}). " + + "Check that SPARK_HOME is set correctly and the bridge JAR is valid." + ); + + Thread.Sleep(500); + } + } + + throw new InvalidOperationException( + $"Spark JVM backend did not become ready on port {port} within {timeoutSeconds}s." + ); + } +} diff --git a/src/extensions/Flowthru.Extensions.Spark/Runtime/SparkRuntimeOptions.cs b/src/extensions/Flowthru.Extensions.Spark/Runtime/SparkRuntimeOptions.cs new file mode 100644 index 00000000..5d4f20b6 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/Runtime/SparkRuntimeOptions.cs @@ -0,0 +1,134 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Flowthru.Extensions.Spark.Runtime; + +/// +/// Configuration options for the Spark runtime. +/// +/// +/// +/// All paths are auto-detected from the executing assembly's output directory and environment +/// variables. Override properties explicitly only when auto-detection is insufficient +/// (e.g., containers with non-standard SPARK_HOME, CI with pre-built JARs elsewhere). +/// +/// +/// Auto-detection hierarchy for : +/// +/// Explicit property +/// SPARK_HOME environment variable +/// Common Homebrew path on macOS (/opt/homebrew/opt/apache-spark/libexec) +/// +/// +/// +/// Auto-detection hierarchy for : +/// +/// Explicit property +/// FLOWTHRU_SPARK_JAR environment variable +/// flowthru-spark-4-1_2.13-2.3.1.jar alongside the executing assembly +/// +/// +/// +public sealed class SparkRuntimeOptions +{ + internal const string JarFileName = "flowthru-spark-4-1_2.13-2.3.1.jar"; + + /// + /// Path to the Spark installation directory (the value of SPARK_HOME). + /// If null, resolved via auto-detection. + /// + public string? SparkHome { get; set; } + + /// + /// Path to the flowthru-spark-*.jar bridge artifact. + /// If null, resolved from the executing assembly's output directory. + /// + public string? JarPath { get; set; } + + /// + /// Spark master URL passed to spark-submit. + /// Defaults to local[*] for in-process execution on all available cores. + /// + public string Master { get; set; } = "local[*]"; + + /// + /// Maximum seconds to wait for the JVM backend to accept connections after launch. + /// Defaults to 60s for production use. Set lower (e.g. 10s) in test fixtures to + /// fail fast when the backend is unavailable. + /// + public int BackendStartupTimeoutSeconds { get; set; } = 60; + + /// + /// Resolves the Spark home directory using the auto-detection hierarchy. + /// + /// + /// Thrown when no Spark installation can be located. + /// + public string GetResolvedSparkHome() + { + if (!string.IsNullOrWhiteSpace(SparkHome)) + return SparkHome!; + + var envSparkHome = Environment.GetEnvironmentVariable("SPARK_HOME"); + if (!string.IsNullOrWhiteSpace(envSparkHome) && Directory.Exists(envSparkHome)) + return envSparkHome!; + + // Common Homebrew path on Apple Silicon and Intel macOS + var homebrewPaths = new[] + { + "/opt/homebrew/opt/apache-spark/libexec", + "/usr/local/opt/apache-spark/libexec", + }; + + foreach (var candidate in homebrewPaths) + { + if (Directory.Exists(candidate)) + return candidate; + } + + throw new InvalidOperationException( + "Spark installation not found. Set the SPARK_HOME environment variable to your " + + "Spark installation directory, or install via 'brew install apache-spark'." + ); + } + + /// + /// Resolves the JVM bridge JAR path using the auto-detection hierarchy. + /// + /// + /// Thrown when the JAR cannot be located. + /// + public string GetResolvedJarPath() + { + if (!string.IsNullOrWhiteSpace(JarPath)) + { + if (!File.Exists(JarPath)) + throw new InvalidOperationException($"Spark bridge JAR not found at explicitly configured path: {JarPath}"); + return JarPath!; + } + + var envJar = Environment.GetEnvironmentVariable("FLOWTHRU_SPARK_JAR"); + if (!string.IsNullOrWhiteSpace(envJar)) + { + if (!File.Exists(envJar)) + throw new InvalidOperationException($"Spark bridge JAR not found at FLOWTHRU_SPARK_JAR path: {envJar}"); + return envJar!; + } + + // JAR is shipped as contentFiles alongside the executing assembly + var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + ?? AppContext.BaseDirectory; + var jarPath = Path.Combine(assemblyDir, JarFileName); + + if (!File.Exists(jarPath)) + throw new InvalidOperationException( + $"Spark bridge JAR '{JarFileName}' not found in assembly output directory '{assemblyDir}'. " + + "Ensure Flowthru.Extensions.Spark was built with its NX 'build' target so the JAR is staged, " + + "or set the FLOWTHRU_SPARK_JAR environment variable explicitly." + ); + + return jarPath; + } +} diff --git a/src/extensions/Flowthru.Extensions.Spark/Services/FlowthruServiceBuilderExtensions.cs b/src/extensions/Flowthru.Extensions.Spark/Services/FlowthruServiceBuilderExtensions.cs new file mode 100644 index 00000000..bc35d0c8 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/Services/FlowthruServiceBuilderExtensions.cs @@ -0,0 +1,86 @@ +using System; +using Flowthru.Core.Services; +using Flowthru.Extensions.Spark.Runtime; +using Microsoft.Extensions.DependencyInjection; + +namespace Flowthru.Extensions.Spark.Services; + +/// +/// Extension methods for integrating Spark support with . +/// +public static class FlowthruServiceBuilderExtensions +{ + /// + /// Registers the Spark runtime with default configuration. + /// + /// The Flowthru service builder. + /// The builder for method chaining. + /// + /// + /// Uses auto-detection for all configuration: + /// + /// JAR: FLOWTHRU_SPARK_JARflowthru-spark-4-1_2.13-2.3.1.jar alongside assembly + /// Spark home: SPARK_HOME → Homebrew default on macOS + /// Master: local[*] + /// + /// + /// + /// Example: + /// + /// services.AddFlowthru(flowthru => + /// { + /// flowthru + /// .RegisterCatalog<MyCatalog>() + /// .UseSpark(); + /// }); + /// + /// + /// + public static FlowthruServiceBuilder UseSpark(this FlowthruServiceBuilder builder) => + builder.UseSpark(_ => { }); + + /// + /// Registers the Spark runtime with custom configuration. + /// + /// The Flowthru service builder. + /// Action to configure Spark runtime options. + /// The builder for method chaining. + /// + /// + /// Example (explicit master for staging cluster): + /// + /// services.AddFlowthru(flowthru => + /// { + /// flowthru + /// .RegisterCatalog<MyCatalog>() + /// .UseSpark(spark => + /// { + /// spark.Master = "spark://staging-cluster:7077"; + /// }); + /// }); + /// + /// + /// + public static FlowthruServiceBuilder UseSpark( + this FlowthruServiceBuilder builder, + Action configure + ) + { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); + + if (configure == null) + throw new ArgumentNullException(nameof(configure)); + + var options = new SparkRuntimeOptions(); + configure(options); + + return builder.ConfigureServices(services => + { + services.AddSingleton(options); + services.AddSingleton(); + services.AddSingleton(); + }); + } +} + diff --git a/src/extensions/Flowthru.Extensions.Spark/Shared/SparkTranslatableOperations.cs b/src/extensions/Flowthru.Extensions.Spark/Shared/SparkTranslatableOperations.cs new file mode 100644 index 00000000..91fda90e --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/Shared/SparkTranslatableOperations.cs @@ -0,0 +1,76 @@ +// This file is shared between Flowthru.Extensions.Spark (runtime) and +// Flowthru.Extensions.Spark.SourceGenerators (analyzer) via a . +// It must not reference types from either host project — only BCL types. + +using System; +using System.Collections.Generic; + +namespace Flowthru.Extensions.Spark.Shared; + +/// +/// The authoritative whitelist of BCL operations that the SparkExpressionVisitor +/// can translate to Spark Column expressions. +/// +/// +/// +/// This class is the single source of truth for the Spark-translatable subset. Both the +/// runtime visitor and the FSPARK1002 Roslyn analyzer consume it. When a new +/// translation is added to SparkExpressionVisitor: +/// +/// +/// Add the method or operator name to the appropriate set here. +/// Implement the switch arm in the visitor. +/// +/// +/// A sync-validation test in Flowthru.Tests.Spark verifies that every entry in +/// these sets has a corresponding switch arm in the visitor, catching the reverse case. +/// +/// +public static class SparkTranslatableOperations +{ + /// + /// string instance methods that have Spark Column translations. + /// + public static readonly IReadOnlyCollection SupportedStringMethods = new HashSet( + StringComparer.Ordinal + ) + { + nameof(string.Replace), + nameof(string.Contains), + nameof(string.StartsWith), + nameof(string.EndsWith), + nameof(string.ToUpper), + nameof(string.ToLower), + nameof(string.Trim), + nameof(string.TrimStart), + nameof(string.TrimEnd), + nameof(string.Substring), + }; + + /// + /// System.Math static methods that have Spark Column translations. + /// + public static readonly IReadOnlyCollection SupportedMathMethods = new HashSet( + StringComparer.Ordinal + ) + { + nameof(Math.Round), + nameof(Math.Abs), + nameof(Math.Floor), + nameof(Math.Ceiling), + }; + + /// + /// System.DateTime instance properties that have Spark date/time function translations. + /// + public static readonly IReadOnlyCollection SupportedDateTimeProperties = + new HashSet(StringComparer.Ordinal) + { + nameof(DateTime.Year), + nameof(DateTime.Month), + nameof(DateTime.Day), + nameof(DateTime.Hour), + nameof(DateTime.Minute), + nameof(DateTime.Second), + }; +} diff --git a/src/extensions/Flowthru.Extensions.Spark/SparkExpressionVisitor.cs b/src/extensions/Flowthru.Extensions.Spark/SparkExpressionVisitor.cs new file mode 100644 index 00000000..d5c48c50 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/SparkExpressionVisitor.cs @@ -0,0 +1,834 @@ +using System.Linq.Expressions; +using System.Reflection; +using Flowthru.Extensions.Spark.Shared; +using Flowthru.Misc.DataFrames; +using Flowthru.Spark.Sql; +using SparkFunctions = Flowthru.Spark.Sql.Functions; + +namespace Flowthru.Extensions.Spark; + +/// +/// Translates expression trees into Spark.NET +/// operations. +/// +/// +/// +/// This visitor handles three categories of translation: +/// +/// +/// +/// Top-level operations (Where, Select, Join) — dispatched by the base +/// . Each produces a new . +/// +/// +/// Sub-expressions (predicates, selectors) — translated into Spark +/// expressions via . +/// Property access becomes column references; binary ops use +/// operator overloads; constants use . +/// +/// +/// Projections (MemberInitExpression, NewExpression) — +/// produce arrays with aliases matching the target schema property names. +/// +/// +/// +internal sealed class SparkExpressionVisitor : FrameExpressionVisitor +{ + private readonly SparkFrameProvider _provider; + + internal SparkExpressionVisitor(SparkFrameProvider provider) + { + _provider = provider; + } + + // ────────────────────────────────────────────── + // Root node: extract native DataFrame + // ────────────────────────────────────────────── + + protected override object TranslateConstant(ConstantExpression node) + { + if (node.Value is null) + throw new InvalidOperationException("Null TypedFrame constant encountered."); + + // The constant is a TypedFrame; retrieve the native DataFrame the provider stored. + return _provider.GetNativeFrame(node.Value); + } + + // ────────────────────────────────────────────── + // Easy: Where (type-preserving filter) + // ────────────────────────────────────────────── + + protected override object TranslateWhere(MethodCallExpression node) + { + // args[0] = source expression, args[1] = quoted predicate + var sourceDf = (DataFrame)CompileExpression(node.Arguments[0]); + var predicate = Unquote(node.Arguments[1]); + + var paramMap = new Dictionary + { + [predicate.Parameters[0]] = sourceDf, + }; + + var condition = TranslateSubExpression(predicate.Body, paramMap); + return sourceDf.Filter(condition); + } + + // ────────────────────────────────────────────── + // Easy: OrderBy / OrderByDescending + // ────────────────────────────────────────────── + + protected override object TranslateOrderBy(MethodCallExpression node, bool descending) + { + var sourceDf = (DataFrame)CompileExpression(node.Arguments[0]); + var keyLambda = Unquote(node.Arguments[1]); + + var paramMap = new Dictionary + { + [keyLambda.Parameters[0]] = sourceDf, + }; + + var keyCol = TranslateSubExpression(keyLambda.Body, paramMap); + var sortCol = descending ? keyCol.Desc() : keyCol; + return sourceDf.Sort(sortCol); + } + + // ────────────────────────────────────────────── + // Easy: Take (row limit) + // ────────────────────────────────────────────── + + protected override object TranslateTake(MethodCallExpression node) + { + var sourceDf = (DataFrame)CompileExpression(node.Arguments[0]); + var count = (int)((ConstantExpression)node.Arguments[1]).Value!; + return sourceDf.Limit(count); + } + + // ────────────────────────────────────────────── + // Easy: Count (scalar) + // ────────────────────────────────────────────── + + protected override object TranslateCount(MethodCallExpression node) + { + var sourceDf = (DataFrame)CompileExpression(node.Arguments[0]); + return sourceDf.Count(); + } + + protected override object TranslateDistinct(MethodCallExpression node) + { + var sourceDf = (DataFrame)CompileExpression(node.Arguments[0]); + return sourceDf.Distinct(); + } + + protected override object TranslateUnion(MethodCallExpression node) + { + var leftDf = (DataFrame)CompileExpression(node.Arguments[0]); + var rightDf = (DataFrame)CompileExpression(node.Arguments[1]); + return leftDf.Union(rightDf); + } + + // ────────────────────────────────────────────── + // Intermediate: Select (type-projecting) + // ────────────────────────────────────────────── + + protected override object TranslateSelect(MethodCallExpression node) + { + // args[0] = source expression, args[1] = quoted selector + var sourceDf = (DataFrame)CompileExpression(node.Arguments[0]); + var selector = Unquote(node.Arguments[1]); + + var paramMap = new Dictionary + { + [selector.Parameters[0]] = sourceDf, + }; + + var columns = TranslateProjection(selector.Body, paramMap); + return sourceDf.Select(columns); + } + + // ────────────────────────────────────────────── + // Hard: Join (multi-frame) + // ────────────────────────────────────────────── + + protected override object TranslateJoin(MethodCallExpression node) + { + // args: [0] outer, [1] inner, [2] outerKey, [3] innerKey, [4] resultSelector + var outerDf = (DataFrame)CompileExpression(node.Arguments[0]); + var innerDf = (DataFrame)CompileExpression(node.Arguments[1]); + var outerKeyLambda = Unquote(node.Arguments[2]); + var innerKeyLambda = Unquote(node.Arguments[3]); + var resultLambda = Unquote(node.Arguments[4]); + + // Translate key selectors to Column references + var outerKeyMap = new Dictionary + { + [outerKeyLambda.Parameters[0]] = outerDf, + }; + var innerKeyMap = new Dictionary + { + [innerKeyLambda.Parameters[0]] = innerDf, + }; + + var outerKeyCol = TranslateSubExpression(outerKeyLambda.Body, outerKeyMap); + var innerKeyCol = TranslateSubExpression(innerKeyLambda.Body, innerKeyMap); + + // Equi-join: outerKey == innerKey + var joinCondition = outerKeyCol == innerKeyCol; + var joinedDf = outerDf.Join(innerDf, joinCondition, "inner"); + + // Translate result selector into projection columns. + // The result lambda has two parameters: (outer, inner). + var resultMap = new Dictionary + { + [resultLambda.Parameters[0]] = outerDf, + [resultLambda.Parameters[1]] = innerDf, + }; + + var resultColumns = TranslateProjection(resultLambda.Body, resultMap); + return joinedDf.Select(resultColumns); + } + + // ────────────────────────────────────────────── + // Medium: GroupBy + Aggregate + // ────────────────────────────────────────────── + + protected override object TranslateGroupBy(MethodCallExpression node) + { + // GroupBy returns a RelationalGroupedDataset — stored as the "native" value in + // the GroupedFrame expression chain, picked up by TranslateAggregate. + var sourceDf = (DataFrame)CompileExpression(node.Arguments[0]); + var keyLambda = Unquote(node.Arguments[1]); + + var paramMap = new Dictionary + { + [keyLambda.Parameters[0]] = sourceDf, + }; + + var keyCol = TranslateSubExpression(keyLambda.Body, paramMap); + return sourceDf.GroupBy(keyCol); + } + + protected override object TranslateAggregate(MethodCallExpression node) + { + // args[0] = GroupBy expression (produces RelationalGroupedDataset) + // args[1] = quoted result selector: AggregationContext → TResult + var grouped = (RelationalGroupedDataset)CompileExpression(node.Arguments[0]); + var resultLambda = Unquote(node.Arguments[1]); + var keyColName = ExtractGroupByKeyColumnName(node.Arguments[0]); + + // Split bindings into: + // aggCols — passed to .Agg(); excludes ctx.Key (Spark projects it automatically) + // outputCols — final Select to reorder/rename all columns to match target schema + var (aggCols, outputCols) = BuildAggregateColumns( + resultLambda.Body, + resultLambda.Parameters[0], + keyColName + ); + + if (aggCols.Count == 0) + throw new NotSupportedException( + "Aggregate result selector must contain at least one aggregate function " + + "(Avg, Sum, Min, Max, or Count). Key-only projections are not supported." + ); + + var agged = grouped.Agg(aggCols[0], aggCols.Skip(1).ToArray()); + + // Reorder/rename to match target schema order + return agged.Select(outputCols.ToArray()); + } + + /// + /// Splits an Aggregate result selector into two lists: + /// + /// aggCols — columns for .Agg(); key bindings excluded because + /// Spark already projects the group-by key into the Agg output automatically. + /// outputCols — all columns in target schema order, for the final + /// .Select() that renames/reorders the Agg result. + /// + /// + private (List aggCols, List outputCols) BuildAggregateColumns( + Expression body, + ParameterExpression contextParam, + string keyColName + ) + { + var aggCols = new List(); + var outputCols = new List(); + + IEnumerable<(Expression expr, MemberInfo targetMember)> bindings = body switch + { + MemberInitExpression mie => mie + .Bindings.Cast() + .Select(b => (b.Expression, b.Member)), + NewExpression ne when ne.Members is not null => ne.Arguments.Zip( + ne.Members, + (a, m) => (a, (MemberInfo)m) + ), + _ => throw new NotSupportedException( + "Aggregate result selector must be a member initializer or positional constructor." + ), + }; + + foreach (var (expr, member) in bindings) + { + var targetName = ResolveColumnName(member); + + if (IsKeyAccess(expr, contextParam)) + { + // The key column is already in the Agg output under keyColName. + // Reference it (with rename if the target property name differs). + outputCols.Add(SparkFunctions.Col(keyColName).As(targetName)); + } + else + { + // Aggregate function — include in Agg() call, then reference by target name in Select. + var aggCol = TranslateAggregateExpression(expr, contextParam, keyColName).As(targetName); + aggCols.Add(aggCol); + outputCols.Add(SparkFunctions.Col(targetName)); + } + } + + return (aggCols, outputCols); + } + + private static bool IsKeyAccess(Expression expr, ParameterExpression contextParam) => + expr is MemberExpression { Member.Name: "Key" } me && me.Expression == contextParam; + + /// + /// Extracts the key column name string from the GroupBy MethodCallExpression that is the + /// source argument of an Aggregate call (i.e., args[0] of the Aggregate node). + /// + private static string ExtractGroupByKeyColumnName(Expression groupByExpr) + { + // groupByExpr may itself be wrapped in a ConstantExpression if already compiled, + // but in the expression tree it is always a MethodCallExpression for GroupBy. + if ( + groupByExpr is MethodCallExpression gbe + && gbe.Method.Name == nameof(TypedFrameExtensions.GroupBy) + ) + { + var keyLambda = Unquote(gbe.Arguments[1]); + if (keyLambda.Body is MemberExpression me) + return ResolveColumnName(me.Member); + } + + throw new NotSupportedException( + "Could not extract key column name from GroupBy source expression. " + + "ctx.Key is only supported when the GroupBy key is a direct property access." + ); + } + + protected override object TranslateSelectOver(MethodCallExpression node) + { + throw new NotImplementedException(); + } + + /// + /// Translates a single aggregate function call on + /// into its Spark equivalent. + /// + /// + /// Key bindings (ctx.Key) are handled upstream in BuildAggregateColumns + /// and must not reach this method. + /// + private Column TranslateAggregateExpression( + Expression expr, + ParameterExpression contextParam, + string keyColName + ) + { + // ctx.Avg(x => x.Price), ctx.Sum(x => x.Quantity), etc. + if ( + expr is MethodCallExpression mce + && mce.Object is ParameterExpression pe + && pe == contextParam + ) + { + var innerColName = ExtractAggregateColumnName(mce.Arguments[0]); + + return mce.Method.Name switch + { + "Avg" => SparkFunctions.Avg(innerColName), + "Sum" => SparkFunctions.Sum(innerColName), + "Max" => SparkFunctions.Max(innerColName), + "Min" => SparkFunctions.Min(innerColName), + "Count" => SparkFunctions.Count(SparkFunctions.Lit(1)), + _ => throw new NotSupportedException( + $"Aggregate function '{mce.Method.Name}' is not supported." + ), + }; + } + + throw new NotSupportedException( + $"Expression '{expr}' in aggregate result selector is not supported. " + + "Only AggregationContext method calls (Avg, Sum, Count, Min, Max) are valid. " + + "ctx.Key bindings are handled separately and should not reach this method." + ); + } + + /// + /// Extracts the column name string from the inner lambda argument of an aggregate call + /// (e.g., the x => x.Price in ctx.Avg(x => x.Price)). + /// + private static string ExtractAggregateColumnName(Expression lambdaExpr) + { + var lambda = lambdaExpr is UnaryExpression { NodeType: ExpressionType.Quote } q + ? (LambdaExpression)q.Operand + : (LambdaExpression)lambdaExpr; + + // Peel off any implicit cast (e.g., (double)s.PassengerCapacity → s.PassengerCapacity) + var body = lambda.Body; + while (body is UnaryExpression { NodeType: ExpressionType.Convert } ue) + body = ue.Operand; + + if (body is MemberExpression me) + return ResolveColumnName(me.Member); + + throw new NotSupportedException( + "Aggregate column selectors must be simple property access expressions (x => x.Property) " + + "or cast expressions ((double)x.Property). " + + $"Got: {lambda.Body.NodeType} ({lambda.Body})." + ); + } + + // ────────────────────────────────────────────── + // Sub-expression translation → Column + // ────────────────────────────────────────────── + + /// + /// Translates a LINQ expression node into a Spark . + /// + /// The expression node (predicate body, selector fragment, etc.). + /// Maps lambda parameters to their backing DataFrames. + private Column TranslateSubExpression( + Expression expr, + Dictionary paramMap + ) + { + return expr switch + { + // Property access on a schema type → column reference + MemberExpression me => TranslateMemberAccess(me, paramMap), + + // Binary operation → Column operator overload + BinaryExpression be => TranslateBinary(be, paramMap), + + // Ternary / conditional → When(...).Otherwise(...) + ConditionalExpression ce => TranslateConditional(ce, paramMap), + + // Method call → string methods, Math methods, etc. + MethodCallExpression mce => TranslateMethodCallExpression(mce, paramMap), + + // Constant value → Lit() + ConstantExpression ce => SparkFunctions.Lit(ce.Value), + + // Implicit cast / convert → recurse through + UnaryExpression { NodeType: ExpressionType.Convert } ue => TranslateSubExpression( + ue.Operand, + paramMap + ), + + // Boolean negation + UnaryExpression { NodeType: ExpressionType.Not } ue => !TranslateSubExpression( + ue.Operand, + paramMap + ), + + _ => throw new NotSupportedException( + $"Expression node type '{expr.NodeType}' ({expr.GetType().Name}) " + + "is not supported in Spark sub-expressions. " + + "Supported: property access, binary operations, constants, negation, " + + "conditionals, and selected string/math method calls." + ), + }; + } + + // ────────────────────────────────────────────── + // Conditional / ternary translation + // ────────────────────────────────────────────── + + /// + /// Translates a ConditionalExpression (x ? a : b) into + /// Functions.When(condition, ifTrue).Otherwise(ifFalse). + /// + private Column TranslateConditional( + ConditionalExpression ce, + Dictionary paramMap + ) + { + var condition = TranslateSubExpression(ce.Test, paramMap); + var ifTrue = (object)TranslateSubExpression(ce.IfTrue, paramMap); + var ifFalse = (object)TranslateSubExpression(ce.IfFalse, paramMap); + return SparkFunctions.When(condition, ifTrue).Otherwise(ifFalse); + } + + // ────────────────────────────────────────────── + // Method call translation (string, Math, etc.) + // ────────────────────────────────────────────── + + // Method name whitelists are defined in SparkTranslatableOperations (shared with the analyzer). + // Update that file when adding new translations — do not add sets here. + + /// + /// Routes a method call in a sub-expression to the appropriate translator. + /// + private Column TranslateMethodCallExpression( + MethodCallExpression mce, + Dictionary paramMap + ) + { + if ( + mce.Method.DeclaringType == typeof(string) + && SparkTranslatableOperations.SupportedStringMethods.Contains(mce.Method.Name) + ) + return TranslateStringMethod(mce, paramMap); + + if ( + mce.Method.DeclaringType == typeof(Math) + && SparkTranslatableOperations.SupportedMathMethods.Contains(mce.Method.Name) + ) + return TranslateMathMethod(mce, paramMap); + + throw new NotSupportedException( + $"Method '{mce.Method.DeclaringType?.Name}.{mce.Method.Name}' " + + "is not supported in Spark sub-expressions. " + + "Supported string methods: Replace, Contains, StartsWith, EndsWith, " + + "ToUpper, ToLower, Trim, TrimStart, TrimEnd, Substring. " + + "Supported Math methods: Round, Abs, Floor, Ceiling." + ); + } + + /// + /// Translates string instance methods to their Spark Column equivalents. + /// + /// + /// Indexing convention: all index and position arguments follow C#'s + /// idiomatic zero-based indexing in the expression passed by the caller. Where the + /// underlying Spark function uses a different convention (e.g., Substring takes a + /// 1-based position), this method adjusts transparently so callers never need to be aware + /// of Spark's internal convention. + /// + private Column TranslateStringMethod( + MethodCallExpression mce, + Dictionary paramMap + ) + { + // All string instance methods have a non-null Object (the receiver) + var col = TranslateSubExpression(mce.Object!, paramMap); + + return mce.Method.Name switch + { + // s.ToUpper() → Upper(col) + nameof(string.ToUpper) => SparkFunctions.Upper(col), + + // s.ToLower() → Lower(col) + nameof(string.ToLower) => SparkFunctions.Lower(col), + + // s.Trim() → Trim(col) + nameof(string.Trim) => SparkFunctions.Trim(col), + + // s.Contains(x) → col.Contains(x) + nameof(string.Contains) => col.Contains(TranslateSubExpression(mce.Arguments[0], paramMap)), + + // s.StartsWith(x) → col.StartsWith(x) + nameof(string.StartsWith) => col.StartsWith( + TranslateSubExpression(mce.Arguments[0], paramMap) + ), + + // s.EndsWith(x) → col.EndsWith(x) + nameof(string.EndsWith) => col.EndsWith(TranslateSubExpression(mce.Arguments[0], paramMap)), + + // s.TrimStart() → Ltrim(col) + nameof(string.TrimStart) => SparkFunctions.Ltrim(col), + + // s.TrimEnd() → Rtrim(col) + nameof(string.TrimEnd) => SparkFunctions.Rtrim(col), + + // s.Substring(startIndex, length) → Substring(col, startIndex + 1, length) + // C# uses zero-based startIndex; Spark's Substring uses 1-based position. + // This translator adjusts transparently — callers always pass zero-based indices. + nameof(string.Substring) => TranslateStringSubstring(mce, col, paramMap), + + // s.Replace(old, new) → RegexpReplace(col, old, new) + // ⚠ SEMANTIC GAP: Spark's RegexpReplace interprets the first argument as a + // regular expression pattern, not a literal string. C#'s string.Replace + // treats it as a literal. Callers using regex-special characters (., *, +, etc.) + // in the search string must escape them (e.g., Regex.Escape) or the translation + // will produce different results than the equivalent in-process C# expression. + nameof(string.Replace) => SparkFunctions.RegexpReplace( + col, + TranslateSubExpression(mce.Arguments[0], paramMap), + TranslateSubExpression(mce.Arguments[1], paramMap) + ), + + _ => throw new NotSupportedException( + $"String method '{mce.Method.Name}' has no Spark translation." + ), + }; + } + + /// + /// Handles string.Substring(startIndex) and string.Substring(startIndex, length), + /// adjusting from C#'s zero-based to Spark's 1-based position. + /// + private Column TranslateStringSubstring( + MethodCallExpression mce, + Column col, + Dictionary paramMap + ) + { + // Spark Substring(col, pos, len): pos is 1-based. + // We add 1 to the caller's 0-based startIndex here so callers never see Spark's convention. + // startIndex must be a compile-time constant (C# string.Substring requires an int literal). + var startIndex = (int)((ConstantExpression)mce.Arguments[0]).Value!; + var oneBasedPos = startIndex + 1; + + if (mce.Arguments.Count == 1) + { + // Substring(startIndex) — no length; use Int32.MaxValue as "rest of string" + return SparkFunctions.Substring(col, oneBasedPos, int.MaxValue); + } + + var length = (int)((ConstantExpression)mce.Arguments[1]).Value!; + return SparkFunctions.Substring(col, oneBasedPos, length); + } + + // ────────────────────────────────────────────── + // Math method translation + // ────────────────────────────────────────────── + + /// + /// Translates System.Math static methods to their Spark Column equivalents. + /// + private Column TranslateMathMethod( + MethodCallExpression mce, + Dictionary paramMap + ) + { + // All supported Math methods take a single numeric argument (the column expression). + // Math.Round has overloads with a decimal-places argument — we support both. + var col = TranslateSubExpression(mce.Arguments[0], paramMap); + + return mce.Method.Name switch + { + // Math.Abs(x) → Abs(col) + nameof(Math.Abs) => SparkFunctions.Abs(col), + + // Math.Floor(x) → Floor(col) + nameof(Math.Floor) => SparkFunctions.Floor(col), + + // Math.Ceiling(x) → Ceil(col) + nameof(Math.Ceiling) => SparkFunctions.Ceil(col), + + // Math.Round(x) → Round(col, 0) + // Math.Round(x, decimals) → Round(col, decimals) + nameof(Math.Round) when mce.Arguments.Count == 1 => SparkFunctions.Round(col, 0), + nameof(Math.Round) => SparkFunctions.Round( + col, + (int)((ConstantExpression)mce.Arguments[1]).Value! + ), + + _ => throw new NotSupportedException( + $"Math method '{mce.Method.Name}' has no Spark translation." + ), + }; + } + + // ────────────────────────────────────────────── + // Member access translation (columns + string.Length) + // ────────────────────────────────────────────── + + /// + /// Translates a member access into a Spark reference. + /// Handles string.Length as a special case before resolving column names. + /// + private Column TranslateMemberAccess( + MemberExpression me, + Dictionary paramMap + ) + { + // string.Length → Length(col) + if ( + me.Member is PropertyInfo { Name: nameof(string.Length) } + && me.Member.DeclaringType == typeof(string) + && me.Expression is not null + ) + { + var inner = TranslateSubExpression(me.Expression, paramMap); + return SparkFunctions.Length(inner); + } + + // DateTime part properties → Spark date/time functions + if ( + me.Member is PropertyInfo dateTimeProp + && me.Member.DeclaringType == typeof(DateTime) + && me.Expression is not null + ) + { + // Supported DateTime properties are listed in SparkTranslatableOperations.SupportedDateTimeProperties. + // Update that file when adding new translations. + var col = TranslateSubExpression(me.Expression, paramMap); + return dateTimeProp.Name switch + { + nameof(DateTime.Year) => SparkFunctions.Year(col), + nameof(DateTime.Month) => SparkFunctions.Month(col), + nameof(DateTime.Day) => SparkFunctions.DayOfMonth(col), + nameof(DateTime.Hour) => SparkFunctions.Hour(col), + nameof(DateTime.Minute) => SparkFunctions.Minute(col), + nameof(DateTime.Second) => SparkFunctions.Second(col), + _ => throw new NotSupportedException( + $"DateTime property '{dateTimeProp.Name}' has no Spark translation. " + + "Supported: Year, Month, Day, Hour, Minute, Second." + ), + }; + } + + // Direct property on a lambda parameter: x.Age → df["Age"] + if (me.Expression is ParameterExpression pe && paramMap.TryGetValue(pe, out var df)) + { + var colName = ResolveColumnName(me.Member); + return df[colName]; + } + + // Closure-captured variable or static field: evaluate to a constant + var value = EvaluateConstant(me); + return SparkFunctions.Lit(value); + } + + /// + /// Translates a binary expression into a Spark operation + /// using Column operator overloads. + /// + private Column TranslateBinary( + BinaryExpression be, + Dictionary paramMap + ) + { + // Null checks: x.Prop == null → IsNull(col), x.Prop != null → IsNotNull(col). + // Spark's col == null produces col = NULL (always false in SQL three-valued logic), + // so we must intercept before translating operands into Column objects. + if (be.NodeType is ExpressionType.Equal or ExpressionType.NotEqual) + { + var isLeftNull = be.Left is ConstantExpression { Value: null }; + var isRightNull = be.Right is ConstantExpression { Value: null }; + + if (isLeftNull || isRightNull) + { + var nonNullSide = isRightNull ? be.Left : be.Right; + var col = TranslateSubExpression(nonNullSide, paramMap); + return be.NodeType == ExpressionType.Equal ? SparkFunctions.IsNull(col) : col.IsNotNull(); + } + } + + var left = TranslateSubExpression(be.Left, paramMap); + var right = TranslateSubExpression(be.Right, paramMap); + + return be.NodeType switch + { + // Comparison + ExpressionType.Equal => left == (object)right, + ExpressionType.NotEqual => left != (object)right, + ExpressionType.GreaterThan => left > (object)right, + ExpressionType.GreaterThanOrEqual => left >= (object)right, + ExpressionType.LessThan => left < (object)right, + ExpressionType.LessThanOrEqual => left <= (object)right, + + // Arithmetic + ExpressionType.Add => left + (object)right, + ExpressionType.Subtract => left - (object)right, + ExpressionType.Multiply => left * (object)right, + ExpressionType.Divide => left / (object)right, + ExpressionType.Modulo => left % (object)right, + + // Logical + ExpressionType.AndAlso => left & right, + ExpressionType.OrElse => left | right, + + _ => throw new NotSupportedException( + $"Binary operator '{be.NodeType}' is not supported in Spark expressions." + ), + }; + } + + // ────────────────────────────────────────────── + // Projection translation → Column[] + // ────────────────────────────────────────────── + + /// + /// Translates a projection expression (MemberInitExpression or NewExpression) + /// into an array of aliased Spark objects for DataFrame.Select. + /// + private Column[] TranslateProjection( + Expression body, + Dictionary paramMap + ) + { + return body switch + { + // new OutputSchema { Name = x.Name, Age = x.Age } + MemberInitExpression mie => TranslateMemberInit(mie, paramMap), + + // new OutputSchema(x.Name, x.Age) — positional constructor + NewExpression ne => TranslateNewExpression(ne, paramMap), + + // Single expression (identity projection: x => x.Name) + _ => [TranslateSubExpression(body, paramMap)], + }; + } + + /// + /// Translates new T { Prop1 = expr1, Prop2 = expr2 } into aliased columns. + /// + private Column[] TranslateMemberInit( + MemberInitExpression mie, + Dictionary paramMap + ) + { + var columns = new Column[mie.Bindings.Count]; + + for (var i = 0; i < mie.Bindings.Count; i++) + { + if (mie.Bindings[i] is not MemberAssignment assignment) + { + throw new NotSupportedException( + $"Only simple property assignments are supported in projections. " + + $"Got {mie.Bindings[i].BindingType} for '{mie.Bindings[i].Member.Name}'." + ); + } + + var col = TranslateSubExpression(assignment.Expression, paramMap); + var targetName = ResolveColumnName(assignment.Member); + columns[i] = col.As(targetName); + } + + return columns; + } + + /// + /// Translates new T(expr1, expr2) into aliased columns, matching constructor + /// parameter positions to the NewExpression.Members metadata. + /// + private Column[] TranslateNewExpression( + NewExpression ne, + Dictionary paramMap + ) + { + if (ne.Members is null || ne.Members.Count == 0) + { + throw new NotSupportedException( + "Positional constructor projections require member metadata. " + + "Use a record type or object initializer syntax." + ); + } + + var columns = new Column[ne.Arguments.Count]; + + for (var i = 0; i < ne.Arguments.Count; i++) + { + var col = TranslateSubExpression(ne.Arguments[i], paramMap); + var targetName = ResolveColumnName(ne.Members[i]); + columns[i] = col.As(targetName); + } + + return columns; + } +} diff --git a/src/extensions/Flowthru.Extensions.Spark/SparkFrameProvider.cs b/src/extensions/Flowthru.Extensions.Spark/SparkFrameProvider.cs new file mode 100644 index 00000000..244246c8 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/SparkFrameProvider.cs @@ -0,0 +1,205 @@ +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using Flowthru.Core.Abstractions; +using Flowthru.Extensions.Spark.Runtime; +using Flowthru.Misc.DataFrames; +using Flowthru.Spark.Sql; + +namespace Flowthru.Extensions.Spark; + +/// +/// An that backs instances +/// with Spark.NET objects. +/// +/// +/// +/// This provider manages the association between phantom wrappers +/// and the native objects they represent. It follows the standard +/// contract: +/// +/// +/// wraps an expression in a new . +/// walks the accumulated expression tree via +/// and produces a native . +/// +/// +public sealed class SparkFrameProvider : IFrameQueryProvider +{ + private readonly ConditionalWeakTable _nativeFrames = new(); + private readonly SparkExpressionVisitor _visitor; + private readonly SparkSession _session; + + /// + /// Initializes a new . + /// + /// + /// Calls to ensure the JVM backend is running before + /// creating the Spark session. Consuming code (catalogs, flows, steps) does not need to + /// interact with or directly. + /// + public SparkFrameProvider(SparkRuntime runtime) + { + runtime.Initialize(); + _session = SparkSession.Builder().GetOrCreate(); + _visitor = new SparkExpressionVisitor(this); + } + + /// + /// Initializes a without starting the JVM backend. + /// For use in unit tests that validate schema logic without a live Spark session. + /// + internal SparkFrameProvider() + { + _session = null!; + _visitor = new SparkExpressionVisitor(this); + } + + /// + /// Creates a root backed by a native Spark . + /// + /// The schema type representing the DataFrame's row structure. + /// The native Spark DataFrame. + /// A typed frame that can be transformed via LINQ-style extension methods. + public TypedFrame CreateFromNative(DataFrame dataFrame) + { + var frame = new TypedFrame(this); + _nativeFrames.AddOrUpdate(frame, dataFrame); + return frame; + } + + /// + /// Creates a root by ingesting an + /// into Spark via the managed session. + /// + /// + /// + /// The is derived automatically from 's + /// property metadata using , so no manual schema + /// declaration is required in the calling code. + /// + /// + /// This is the standard entry point for preprocessing steps that produce typed data from + /// raw sources. Steps calling this method do not need to + /// reference , , + /// or directly. + /// + /// + /// A flat schema type whose properties define the Spark column layout. + /// The rows to ingest. Enumerated once. + /// A typed frame backed by the ingested DataFrame. + public TypedFrame CreateFromEnumerable(IEnumerable source) + where T : notnull, IFlatSchema + { + var schema = SparkSchemaInference.InferStructType(); + var rows = SparkSchemaInference.ToGenericRows(source); + var df = _session.CreateDataFrame(rows, schema); + return CreateFromNative(df); + } + + /// + /// Retrieves the native associated with a root + /// . + /// + internal DataFrame GetNativeFrame(object frame) + { + if (_nativeFrames.TryGetValue(frame, out var df)) + return df; + + throw new InvalidOperationException( + "TypedFrame is not associated with a native Spark DataFrame. " + + "Root frames must be created via SparkFrameProvider.CreateFromNative()." + ); + } + + /// + /// Compiles the expression tree into a native Spark . + /// + /// + /// The accumulated expression tree from chained operations. + /// + /// A Spark . + public DataFrame CompileToNative(Expression expression) + { + return (DataFrame)Compile(expression); + } + + /// + /// Compiles the expression tree of a into a native DataFrame. + /// + public DataFrame CompileToNative(TypedFrame frame) + { + return CompileToNative(frame.Expression); + } + + // ────────────────────────────────────────────── + // IFrameQueryProvider + // ────────────────────────────────────────────── + + /// + public object Compile(Expression expression) + { + return _visitor.CompileExpression(expression); + } + + /// + /// + /// + /// carries an IFlatSchema constraint that cannot + /// be expressed on this method. The constraint is already enforced at catalog construction + /// via , so every + /// in the system is guaranteed to carry a flat schema at runtime. + /// + /// + /// The hydrator is instantiated via reflection to bypass the compile-time constraint. + /// + /// + public IEnumerable Materialize(Expression expression) + { + var df = CompileToNative(expression); + var hydratorType = typeof(SparkRowHydrator<>).MakeGenericType(typeof(T)); + var hydrator = Activator.CreateInstance(hydratorType, this)!; + var collectRows = hydratorType.GetMethod( + "CollectRows", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance + )!; + return (IEnumerable)collectRows.Invoke(hydrator, [df])!; + } + + // ────────────────────────────────────────────── + // IQueryProvider + // ────────────────────────────────────────────── + + /// + public IQueryable CreateQuery(Expression expression) + { + return new TypedFrame(this, expression); + } + + /// + public IQueryable CreateQuery(Expression expression) + { + var elementType = + expression.Type.GetGenericArguments().FirstOrDefault() + ?? throw new ArgumentException( + "Cannot determine element type from expression.", + nameof(expression) + ); + + var frameType = typeof(TypedFrame<>).MakeGenericType(elementType); + return (IQueryable)Activator.CreateInstance(frameType, this, expression)!; + } + + /// + /// Executes a scalar terminal operation (e.g., Count()) by compiling the + /// expression tree and returning the result. + /// + public TResult Execute(Expression expression) + { + var result = _visitor.CompileExpression(expression); + return (TResult)result; + } + + /// Not supported — use the generic overload. + public object? Execute(Expression expression) => + throw new NotSupportedException("Use Execute for scalar terminal operations."); +} diff --git a/src/extensions/Flowthru.Extensions.Spark/SparkRowHydrator.cs b/src/extensions/Flowthru.Extensions.Spark/SparkRowHydrator.cs new file mode 100644 index 00000000..bb7fc3aa --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/SparkRowHydrator.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Flowthru.Core.Abstractions; +using Flowthru.Core.Data.Storage.Format; +using Flowthru.Misc.DataFrames; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Extensions.Spark; + +/// +/// Represents a schema mismatch detected during pre-flight validation of a Spark +/// against a typed schema . +/// +public sealed record SchemaValidationError(string ColumnName, string Reason); + +/// +/// Materializes a into an by +/// compiling the expression tree, validating the live Spark schema, and collecting rows. +/// +/// +/// The target schema type. Must be a flat schema — non-flat schemas contain +/// nested or collection properties that cannot be expressed as scalar Spark columns. +/// +/// +/// +/// This is the materialization boundary in a Spark flow. When a step inputs a +/// and outputs an , call +/// to trigger the Spark action and hydrate typed rows. +/// +/// +/// The is used to resolve column names, so +/// [SerializedLabel] attributes are honoured the same way as CSV and Parquet. +/// +/// +/// Register via UseSpark(); inject into flow delegates by type parameter. +/// +/// +public sealed class SparkRowHydrator + where T : notnull, IFlatSchema +{ + private readonly SparkFrameProvider _provider; + + // Built once per T — reflection cost is paid at construction time. + private readonly IReadOnlyDictionary _propertyMap; + + // Maps each Spark DataType to the C# types that can safely receive it. + private static readonly IReadOnlyDictionary> s_sparkToClrCompatibility = + BuildCompatibilityMap(); + + /// + /// Initializes a new . + /// + public SparkRowHydrator(SparkFrameProvider provider) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _propertyMap = PropertyMappingHelper.BuildPropertyMap(); + } + + // ────────────────────────────────────────────────────────────────── + // Pre-flight validation + // ────────────────────────────────────────────────────────────────── + + /// + /// Validates that a covers every property on + /// with a compatible type. + /// + /// + /// This overload works without a live JVM and is the testable core of schema validation. + /// Use it in pre-flight checks or unit tests where a is not available. + /// + /// The Spark struct schema to validate against . + /// + /// An empty list when the schema is compatible; otherwise, one entry per problem column. + /// + public IReadOnlyList ValidateSchema(StructType schema) + { + if (schema == null) + throw new ArgumentNullException(nameof(schema)); + + var errors = new List(); + var schemaFieldsByName = schema.Fields.ToDictionary( + f => f.Name, + StringComparer.OrdinalIgnoreCase + ); + + foreach (var (externalName, property) in _propertyMap) + { + if (!schemaFieldsByName.TryGetValue(externalName, out var field)) + { + errors.Add( + new SchemaValidationError( + externalName, + $"Column '{externalName}' (mapped from '{property.Name}') " + + "is not present in the DataFrame schema." + ) + ); + continue; + } + + if (!IsCompatible(field.DataType, property.PropertyType)) + { + errors.Add( + new SchemaValidationError( + externalName, + $"Column '{externalName}' has Spark type '{field.DataType.SimpleString}' " + + $"which is not compatible with C# property '{property.Name}' " + + $"of type '{property.PropertyType.Name}'." + ) + ); + } + } + + return errors; + } + + /// + /// Validates that the 's schema covers every property on + /// with a compatible type. + /// + /// + /// Delegates to after extracting the live schema. + /// Call this before to surface schema drift as a structured + /// pre-flight error rather than a runtime exception deep inside GetAs<T>. + /// + /// The compiled native DataFrame to inspect. + /// + /// An empty list when the schema is compatible; otherwise, one entry per problem column. + /// + public IReadOnlyList ValidateSchema(DataFrame dataFrame) + { + if (dataFrame == null) + throw new ArgumentNullException(nameof(dataFrame)); + + return ValidateSchema(dataFrame.Schema()); + } + + // ────────────────────────────────────────────────────────────────── + // Materialization + // ────────────────────────────────────────────────────────────────── + + /// + /// Compiles the expression tree, validates the live DataFrame schema, collects + /// all rows, and hydrates them into instances. + /// + /// The typed frame to materialize. + /// The materialized rows as an . + /// + /// Thrown when the DataFrame schema is incompatible with . + /// The exception message lists all detected mismatches. + /// + public IEnumerable Collect(TypedFrame frame) + { + if (frame == null) + throw new ArgumentNullException(nameof(frame)); + + var dataFrame = _provider.CompileToNative(frame); + + var errors = ValidateSchema(dataFrame); + if (errors.Count > 0) + { + var details = string.Join( + Environment.NewLine, + errors.Select(e => $" • {e.ColumnName}: {e.Reason}") + ); + throw new InvalidOperationException( + $"DataFrame schema is incompatible with '{typeof(T).Name}':" + Environment.NewLine + details + ); + } + + return dataFrame.Collect().Select(HydrateRow); + } + + /// + /// Validates the schema of an already-compiled , collects + /// all rows, and hydrates them into instances. + /// + /// + /// Called by to support transparent + /// TypedFrame → IEnumerable conversion at catalog item boundaries. + /// + /// The compiled native DataFrame. + /// The materialized rows as an . + internal IEnumerable CollectRows(DataFrame dataFrame) + { + if (dataFrame == null) + throw new ArgumentNullException(nameof(dataFrame)); + + var errors = ValidateSchema(dataFrame); + if (errors.Count > 0) + { + var details = string.Join( + Environment.NewLine, + errors.Select(e => $" • {e.ColumnName}: {e.Reason}") + ); + throw new InvalidOperationException( + $"DataFrame schema is incompatible with '{typeof(T).Name}':" + Environment.NewLine + details + ); + } + + return dataFrame.Collect().Select(HydrateRow); + } + + // ────────────────────────────────────────────────────────────────── + // Row → T hydration + // ────────────────────────────────────────────────────────────────── + + private T HydrateRow(Row row) + { + // Records with required init properties cannot be set via reflection after construction, + // so we build a Dictionary and use the ObjectFactory pattern: + // construct via the primary constructor by matching parameter names. + // + // If T has a parameterless constructor (unusual for schema records), fall back to + // property-setter hydration. + var ctor = FindRecordConstructor(); + if (ctor != null) + return HydrateViaConstructor(row, ctor); + + return HydrateViaSetters(row); + } + + private T HydrateViaConstructor(Row row, ConstructorInfo ctor) + { + var parameters = ctor.GetParameters(); + var args = new object?[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + var param = parameters[i]; + // Constructor parameter names match property names (C# record convention). + // Look up the external field name via the property map. + if ( + _propertyMap.TryGetValue(param.Name!, out var property) + || _propertyMap.TryGetValue( + _propertyMap.Keys.FirstOrDefault(k => + string.Equals(k, param.Name, StringComparison.OrdinalIgnoreCase) + ) ?? "", + out property + ) + ) + { + var externalName = PropertyMappingHelper.GetFieldName(property); + args[i] = ConvertValue(row.Get(externalName), property.PropertyType); + } + else + { + args[i] = param.HasDefaultValue ? param.DefaultValue : null; + } + } + + return (T)ctor.Invoke(args); + } + + private T HydrateViaSetters(Row row) + { + var instance = Activator.CreateInstance(); + foreach (var (externalName, property) in _propertyMap) + { + if (!property.CanWrite) + continue; + + var rawValue = row.Get(externalName); + property.SetValue(instance, ConvertValue(rawValue, property.PropertyType)); + } + return instance; + } + + private ConstructorInfo? FindRecordConstructor() + { + // C# records emit a primary constructor whose parameter count matches property count. + // Pick the constructor whose parameter names collectively match the property map keys + // (by property name, not external label). + var properties = typeof(T) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(p => p.Name) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + return typeof(T) + .GetConstructors(BindingFlags.Public | BindingFlags.Instance) + .Where(c => + { + var ps = c.GetParameters(); + return ps.Length > 0 && ps.All(p => properties.Contains(p.Name ?? "")); + }) + .OrderByDescending(c => c.GetParameters().Length) + .FirstOrDefault(); + } + + // ────────────────────────────────────────────────────────────────── + // Type compatibility and conversion + // ────────────────────────────────────────────────────────────────── + + private static object? ConvertValue(object? raw, Type targetType) + { + if (raw == null) + return null; + + var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; + + if (underlying.IsEnum) + return Enum.Parse(underlying, raw.ToString()!); + + try + { + return Convert.ChangeType(raw, underlying); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Cannot convert Spark value '{raw}' ({raw.GetType().Name}) " + + $"to target type '{underlying.Name}'.", + ex + ); + } + } + + private static bool IsCompatible(DataType sparkType, Type clrType) + { + var underlying = Nullable.GetUnderlyingType(clrType) ?? clrType; + + if (underlying.IsEnum) + return sparkType is StringType or IntegerType or LongType; + + if (s_sparkToClrCompatibility.TryGetValue(sparkType.GetType(), out var compatibleClrTypes)) + return compatibleClrTypes.Contains(underlying); + + // Unknown Spark type — allow through; runtime will report the real error. + return true; + } + + private static IReadOnlyDictionary> BuildCompatibilityMap() + { + return new Dictionary> + { + [typeof(StringType)] = [typeof(string)], + [typeof(BooleanType)] = [typeof(bool)], + [typeof(ByteType)] = [typeof(byte), typeof(sbyte), typeof(short), typeof(int), typeof(long)], + [typeof(ShortType)] = [typeof(short), typeof(int), typeof(long)], + [typeof(IntegerType)] = [typeof(int), typeof(long)], + [typeof(LongType)] = [typeof(long)], + [typeof(FloatType)] = [typeof(float), typeof(double)], + [typeof(DoubleType)] = [typeof(double)], + [typeof(DecimalType)] = [typeof(decimal), typeof(double)], + [typeof(BinaryType)] = [typeof(byte[])], + [typeof(DateType)] = [typeof(DateTime), typeof(DateOnly)], + [typeof(TimestampType)] = [typeof(DateTime), typeof(DateTimeOffset)], + }; + } +} diff --git a/src/extensions/Flowthru.Extensions.Spark/SparkSchemaInference.cs b/src/extensions/Flowthru.Extensions.Spark/SparkSchemaInference.cs new file mode 100644 index 00000000..326349d1 --- /dev/null +++ b/src/extensions/Flowthru.Extensions.Spark/SparkSchemaInference.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Flowthru.Core.Abstractions; +using Flowthru.Core.Data.Storage.Format; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Extensions.Spark; + +/// +/// Derives Spark schema information from types using +/// [SerializedLabel] metadata and a canonical CLR-to-Spark type mapping. +/// +/// +/// Eliminates the need for manual declarations and +/// construction in preprocessing steps. The same metadata +/// that drives CSV/Parquet serialization () is +/// reused here, so column names are always consistent with the rest of the pipeline. +/// +public static class SparkSchemaInference +{ + // CLR → canonical Spark DataType + // decimal maps to DoubleType: the .NET Spark bridge has no DecimalType primitive. + private static readonly IReadOnlyDictionary> s_clrToSpark = + new Dictionary> + { + [typeof(string)] = () => new StringType(), + [typeof(bool)] = () => new BooleanType(), + [typeof(byte)] = () => new ByteType(), + [typeof(sbyte)] = () => new ByteType(), + [typeof(short)] = () => new ShortType(), + [typeof(int)] = () => new IntegerType(), + [typeof(long)] = () => new LongType(), + [typeof(float)] = () => new FloatType(), + [typeof(double)] = () => new DoubleType(), + [typeof(decimal)] = () => new DoubleType(), + [typeof(byte[])] = () => new BinaryType(), + [typeof(DateTime)] = () => new TimestampType(), + [typeof(DateTimeOffset)] = () => new TimestampType(), + [typeof(DateOnly)] = () => new DateType(), + }; + + /// + /// Derives a from the property metadata of + /// . + /// + /// A flat schema type decorated with [FlowthruSchema]. + /// A whose fields mirror the schema properties. + /// + /// Thrown when a property type has no known Spark equivalent. + /// + public static StructType InferStructType() + where T : notnull, IFlatSchema + { + var propertyMap = PropertyMappingHelper.BuildPropertyMap(); + + var fields = propertyMap + .Select(kvp => + { + var columnName = kvp.Key; + var propertyType = kvp.Value.PropertyType; + var underlying = Nullable.GetUnderlyingType(propertyType) ?? propertyType; + var nullable = underlying != propertyType; + + if (!s_clrToSpark.TryGetValue(underlying, out var factory)) + throw new NotSupportedException( + $"Property '{kvp.Value.Name}' on '{typeof(T).Name}' has type " + + $"'{underlying.Name}' which has no known Spark counterpart. " + + "Use a supported CLR type (string, bool, int, long, float, " + + "double, decimal, byte[], DateTime, DateOnly)." + ); + + return new StructField(columnName, factory(), nullable); + }) + .ToArray(); + + return new StructType(fields); + } + + /// + /// Projects an into instances + /// whose values are ordered to match the field order of . + /// + /// A flat schema type decorated with [FlowthruSchema]. + /// The rows to project. + /// + /// An enumerable of objects ready for + /// SparkSession.CreateDataFrame. + /// + public static IEnumerable ToGenericRows(IEnumerable source) + where T : notnull, IFlatSchema + { + var propertyMap = PropertyMappingHelper.BuildPropertyMap(); + var orderedGetters = propertyMap.Values + .Select(p => p.GetGetMethod()!) + .ToArray(); + + foreach (var item in source) + { + var values = new object?[orderedGetters.Length]; + for (var i = 0; i < orderedGetters.Length; i++) + values[i] = orderedGetters[i].Invoke(item, null); + + yield return new GenericRow(values); + } + } +} diff --git a/src/misc/Flowthru.Misc.DataFrames.CodeFixes/Flowthru.Misc.DataFrames.CodeFixes.csproj b/src/misc/Flowthru.Misc.DataFrames.CodeFixes/Flowthru.Misc.DataFrames.CodeFixes.csproj new file mode 100644 index 00000000..78f4b703 --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames.CodeFixes/Flowthru.Misc.DataFrames.CodeFixes.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + latest + enable + true + Flowthru.Misc.DataFrames.CodeFixes + + RS2008;RS2007 + true + false + false + + + + + + + + + + + + diff --git a/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/AnalyzerReleases.Shipped.md b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..5150bd16 --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1,9 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +## Release 1.0 + +### New Rules + +| Rule ID | Category | Severity | Notes | +| ------- | -------- | -------- | ----- | diff --git a/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/AnalyzerReleases.Unshipped.md b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..d2848a86 --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,12 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +| Rule ID | Category | Severity | Notes | +| ------------ | ------------------------ | -------- | ---------------------------------------------------------------------------- | +| FDFRAMES1001 | Flowthru.Misc.DataFrames | Error | TypedFrame Select projection body must be an object initializer | +| FDFRAMES1002 | Flowthru.Misc.DataFrames | Error | TypedFrame Select initializer must use property-assignment bindings | +| FDFRAMES1003 | Flowthru.Misc.DataFrames | Error | TypedFrame Select positional constructor requires a record or anonymous type | +| FDFRAMES1004 | Flowthru.Misc.DataFrames | Error | TypedFrame Aggregate result selector must be an object initializer | +| FDFRAMES1005 | Flowthru.Misc.DataFrames | Error | TypedFrame Aggregate binding must be ctx.Key or an aggregation method call | diff --git a/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/DataFrameDiagnostics.cs b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/DataFrameDiagnostics.cs new file mode 100644 index 00000000..1433bb5b --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/DataFrameDiagnostics.cs @@ -0,0 +1,115 @@ +using Microsoft.CodeAnalysis; + +namespace Flowthru.Misc.DataFrames.Analyzers; + +/// +/// Diagnostic descriptors for the DataFrame expression analyzer. +/// +public static class DataFrameDiagnostics +{ + private const string Category = "Flowthru.Misc.DataFrames"; + + /// + /// FDFRAMES1001: The lambda body passed to TypedFrame.Select() must be an + /// object-creation expression with an initializer, a record/anonymous-type positional + /// constructor call, or a single member access. Arbitrary expression bodies cannot be + /// decomposed into named column operations by any DataFrame provider. + /// + public static readonly DiagnosticDescriptor InvalidProjectionBody = + new( + id: "FDFRAMES1001", + title: "TypedFrame Select projection must be an object initializer or record constructor", + messageFormat: "The Select lambda body '{0}' cannot be translated to named column " + + "operations. Use an object initializer (new OutputSchema {{ Prop = x.Prop }}), " + + "a record constructor, or an anonymous type.", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "TypedFrame.Select() requires the lambda body to be decomposable into " + + "named column operations. Object initializers, record constructors, and anonymous " + + "type constructors are supported. Arbitrary method calls, tuple constructors, or " + + "other expression forms are not translatable by any DataFrame provider." + ); + + /// + /// FDFRAMES1002: An object-initializer binding inside TypedFrame.Select() uses a + /// collection or nested-object form that cannot be decomposed into a single named column + /// operation. Only plain property-assignment bindings (Prop = expr) are translatable. + /// + public static readonly DiagnosticDescriptor NonAssignmentBinding = + new( + id: "FDFRAMES1002", + title: "TypedFrame Select initializer must use property-assignment bindings", + messageFormat: "The binding '{0}' in the Select initializer uses a collection or " + + "nested-object form. Only property-assignment bindings (Prop = expr) can be " + + "translated to named column operations.", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "TypedFrame.Select() initializer bindings must be directly assignable " + + "member expressions. Collection initializers (Items = {{ ... }}) and nested-object " + + "initializers (Nested = {{ Prop = val }}) produce non-assignment expression tree " + + "bindings that no DataFrame provider can translate." + ); + + /// + /// FDFRAMES1003: A positional constructor call inside TypedFrame.Select() targets a + /// type that is not a record or anonymous type. Column names cannot be derived from + /// constructor parameter position for plain classes. + /// + public static readonly DiagnosticDescriptor PositionalConstructorNonRecord = + new( + id: "FDFRAMES1003", + title: "TypedFrame Select positional constructor requires a record or anonymous type", + messageFormat: "'{0}' is not a record or anonymous type. Positional constructors cannot " + + "be decomposed into named column operations unless the type exposes member metadata " + + "(records and anonymous types do; plain classes do not).", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "TypedFrame.Select() with a positional constructor requires the target type " + + "to be a record or anonymous type so that column names can be inferred from the " + + "constructor parameters. Convert the type to a record or use an object initializer." + ); + + /// + /// FDFRAMES1004: The result selector body passed to GroupedFrame.Aggregate() is not + /// an object-creation expression. Aggregate projections must name each output column + /// explicitly via an object initializer, record constructor, or anonymous type. + /// + public static readonly DiagnosticDescriptor InvalidAggregateResultBody = + new( + id: "FDFRAMES1004", + title: "TypedFrame Aggregate result selector must be an object initializer", + messageFormat: "The Aggregate result selector body '{0}' cannot be translated. Use an " + + "object initializer (new TResult {{ Prop = ctx.Key }}), a positional record " + + "constructor, or an anonymous type.", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "GroupedFrame.Aggregate() requires the result selector to produce an object " + + "whose properties map explicitly to aggregation outputs. Member-access passthrough " + + "and arbitrary expressions are not supported — each output column must name a key " + + "or aggregation function from the AggregationContext." + ); + + /// + /// FDFRAMES1005: A binding inside a GroupedFrame.Aggregate() result selector is + /// neither a key access (ctx.Key) nor an aggregation method call + /// (ctx.Avg(...), ctx.Sum(...), etc.). + /// + public static readonly DiagnosticDescriptor InvalidAggregateBinding = + new( + id: "FDFRAMES1005", + title: "TypedFrame Aggregate binding must be ctx.Key or an aggregation method call", + messageFormat: "The expression '{0}' cannot be translated as an aggregate output. Each " + + "property in an Aggregate result selector must be ctx.Key or a call to an " + + "aggregation method (ctx.Avg(...), ctx.Sum(...), ctx.Count(), etc.).", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "GroupedFrame.Aggregate() result selector bindings must either read the " + + "group key (ctx.Key) or invoke an aggregation function on the context. Arbitrary " + + "expressions cannot be translated by any DataFrame provider." + ); +} diff --git a/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/Flowthru.Misc.DataFrames.SourceGenerators.csproj b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/Flowthru.Misc.DataFrames.SourceGenerators.csproj new file mode 100644 index 00000000..da03f2ce --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/Flowthru.Misc.DataFrames.SourceGenerators.csproj @@ -0,0 +1,26 @@ + + + + netstandard2.0 + latest + enable + true + Flowthru.Misc.DataFrames.SourceGenerators + true + RS2008;RS2007 + true + false + false + + + + + + + + + + + + + diff --git a/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/TypedFrameExpressionAnalyzer.cs b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/TypedFrameExpressionAnalyzer.cs new file mode 100644 index 00000000..1dc1c3fc --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/TypedFrameExpressionAnalyzer.cs @@ -0,0 +1,353 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Flowthru.Misc.DataFrames.Analyzers; + +/// +/// Validates that lambda expressions passed to TypedFrameExtensions and +/// GroupedFrameExtensions methods have structurally translatable bodies — +/// constraints that apply regardless of the backing DataFrame provider. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class TypedFrameExpressionAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create( + DataFrameDiagnostics.InvalidProjectionBody, + DataFrameDiagnostics.NonAssignmentBinding, + DataFrameDiagnostics.PositionalConstructorNonRecord, + DataFrameDiagnostics.InvalidAggregateResultBody, + DataFrameDiagnostics.InvalidAggregateBinding + ); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + var invocation = (InvocationExpressionSyntax)context.Node; + var match = TypedFrameInvocationHelper.TryMatch(invocation, context.SemanticModel); + + if (match is null) + return; + + switch (match.Method.Name) + { + case "Select": + CheckSelectLambda(context, match); + break; + case "Aggregate": + CheckAggregateLambda(context, match); + break; + } + } + + // ─── Select checks (FDFRAMES1001, FDFRAMES1002, FDFRAMES1003) ────────────────── + + private static void CheckSelectLambda( + SyntaxNodeAnalysisContext context, + TypedFrameInvocation match + ) + { + if (match.LambdaArguments.Count == 0) + return; + + var selector = match.LambdaArguments[0]; + var body = GetLambdaBody(selector); + if (body is null) + return; + + // FDFRAMES1001: body must be a recognised projection form. + if (!IsValidProjectionBody(body)) + { + context.ReportDiagnostic( + Diagnostic.Create( + DataFrameDiagnostics.InvalidProjectionBody, + body.GetLocation(), + body.ToString() + ) + ); + return; + } + + // FDFRAMES1002: every binding in an object initializer must be a plain assignment. + var initializer = GetObjectInitializer(body); + if (initializer is not null) + CheckInitializerBindings(context, initializer); + + // FDFRAMES1003: positional constructor (no initializer) requires a record or anonymous type. + if (HasPositionalConstructorWithoutInitializer(body)) + CheckPositionalConstructorType(context, body); + } + + // ─── Aggregate checks (FDFRAMES1004, FDFRAMES1005) ──────────────────────────── + + private static void CheckAggregateLambda( + SyntaxNodeAnalysisContext context, + TypedFrameInvocation match + ) + { + if (match.LambdaArguments.Count == 0) + return; + + var resultSelector = match.LambdaArguments[0]; + var body = GetLambdaBody(resultSelector); + if (body is null) + return; + + // FDFRAMES1004: result selector body must be an object-creation expression. + if (!IsValidAggregateResultBody(body)) + { + context.ReportDiagnostic( + Diagnostic.Create( + DataFrameDiagnostics.InvalidAggregateResultBody, + body.GetLocation(), + body.ToString() + ) + ); + return; + } + + // FDFRAMES1005: every binding value must be ctx.Key or ctx.Method(...). + CheckAggregateBindings(context, body, resultSelector); + } + + // ─── Shared projection helpers ─────────────────────────────────────────────── + + private static ExpressionSyntax? GetLambdaBody(LambdaExpressionSyntax lambda) => + lambda switch + { + SimpleLambdaExpressionSyntax s => s.Body as ExpressionSyntax, + ParenthesizedLambdaExpressionSyntax p => p.Body as ExpressionSyntax, + _ => null, + }; + + private static bool IsValidProjectionBody(ExpressionSyntax body) => + body switch + { + // new OutputSchema { Prop = ... } + ObjectCreationExpressionSyntax { Initializer: not null } => true, + // new OutputSchema(...) — positional + ObjectCreationExpressionSyntax { ArgumentList.Arguments.Count: > 0 } => true, + // new { Prop = ... } — anonymous type + AnonymousObjectCreationExpressionSyntax => true, + // new(...) { Prop = ... } — implicit new with initializer (C# 9+) + ImplicitObjectCreationExpressionSyntax { Initializer: not null } => true, + // new(...) — implicit new positional + ImplicitObjectCreationExpressionSyntax { ArgumentList.Arguments.Count: > 0 } => true, + // x.Property — member access passthrough + MemberAccessExpressionSyntax => true, + // x — identifier passthrough + IdentifierNameSyntax => true, + _ => false, + }; + + private static bool IsValidAggregateResultBody(ExpressionSyntax body) => + body switch + { + ObjectCreationExpressionSyntax { Initializer: not null } => true, + ObjectCreationExpressionSyntax { ArgumentList.Arguments.Count: > 0 } => true, + AnonymousObjectCreationExpressionSyntax => true, + ImplicitObjectCreationExpressionSyntax { Initializer: not null } => true, + ImplicitObjectCreationExpressionSyntax { ArgumentList.Arguments.Count: > 0 } => true, + _ => false, + }; + + private static InitializerExpressionSyntax? GetObjectInitializer(ExpressionSyntax body) => + body switch + { + ObjectCreationExpressionSyntax oc => oc.Initializer, + ImplicitObjectCreationExpressionSyntax ic => ic.Initializer, + _ => null, + }; + + private static bool HasPositionalConstructorWithoutInitializer(ExpressionSyntax body) => + body switch + { + ObjectCreationExpressionSyntax { Initializer: null } oc => ( + oc.ArgumentList?.Arguments.Count ?? 0 + ) > 0, + ImplicitObjectCreationExpressionSyntax { Initializer: null } ic => ( + ic.ArgumentList?.Arguments.Count ?? 0 + ) > 0, + _ => false, + }; + + // ─── FDFRAMES1002 ───────────────────────────────────────────────────────────── + + private static void CheckInitializerBindings( + SyntaxNodeAnalysisContext context, + InitializerExpressionSyntax initializer + ) + { + foreach (var expr in initializer.Expressions) + { + // Fire when an assignment's RHS is itself an initializer expression — + // this corresponds to a MemberListBinding (Items = { x }) or + // MemberMemberBinding (Nested = { Prop = val }) in the expression tree. + if (expr is AssignmentExpressionSyntax { Right: InitializerExpressionSyntax } assignment) + { + context.ReportDiagnostic( + Diagnostic.Create( + DataFrameDiagnostics.NonAssignmentBinding, + assignment.GetLocation(), + assignment.Left.ToString() + ) + ); + } + } + } + + // ─── FDFRAMES1003 ───────────────────────────────────────────────────────────── + + private static void CheckPositionalConstructorType( + SyntaxNodeAnalysisContext context, + ExpressionSyntax body + ) + { + var typeInfo = context.SemanticModel.GetTypeInfo(body); + if (typeInfo.Type is INamedTypeSymbol { IsRecord: false, IsAnonymousType: false } namedType) + { + context.ReportDiagnostic( + Diagnostic.Create( + DataFrameDiagnostics.PositionalConstructorNonRecord, + body.GetLocation(), + namedType.Name + ) + ); + } + } + + // ─── FDFRAMES1005 ───────────────────────────────────────────────────────────── + + private static void CheckAggregateBindings( + SyntaxNodeAnalysisContext context, + ExpressionSyntax body, + LambdaExpressionSyntax lambda + ) + { + var ctxParamName = GetFirstParameterName(lambda); + if (ctxParamName is null) + return; + + var ctxSymbol = GetFirstParameterSymbol(lambda, context.SemanticModel); + + foreach (var valueExpr in GetAggregateBindingValues(body)) + { + if (!IsValidAggregateBindingValue(valueExpr, ctxSymbol, ctxParamName, context.SemanticModel)) + { + context.ReportDiagnostic( + Diagnostic.Create( + DataFrameDiagnostics.InvalidAggregateBinding, + valueExpr.GetLocation(), + valueExpr.ToString() + ) + ); + } + } + } + + private static string? GetFirstParameterName(LambdaExpressionSyntax lambda) => + lambda switch + { + SimpleLambdaExpressionSyntax s => s.Parameter.Identifier.Text, + ParenthesizedLambdaExpressionSyntax { ParameterList.Parameters.Count: > 0 } p => + p.ParameterList.Parameters[0].Identifier.Text, + _ => null, + }; + + private static IParameterSymbol? GetFirstParameterSymbol( + LambdaExpressionSyntax lambda, + SemanticModel semanticModel + ) => + lambda switch + { + SimpleLambdaExpressionSyntax s => semanticModel.GetDeclaredSymbol(s.Parameter), + ParenthesizedLambdaExpressionSyntax { ParameterList.Parameters.Count: > 0 } p => + semanticModel.GetDeclaredSymbol(p.ParameterList.Parameters[0]), + _ => null, + }; + + private static IReadOnlyList GetAggregateBindingValues(ExpressionSyntax body) + { + var results = new List(); + + // Object initializer: new TResult { Prop = expr, ... } or new(...) { Prop = expr, ... } + var init = GetObjectInitializer(body); + if (init is not null) + { + foreach (var expr in init.Expressions) + if (expr is AssignmentExpressionSyntax a) + results.Add(a.Right); + return results; + } + + // Anonymous type: new { ctx.Key, Name = ctx.Avg(...) } + if (body is AnonymousObjectCreationExpressionSyntax anon) + { + foreach (var d in anon.Initializers) + results.Add(d.Expression); + return results; + } + + // Positional constructor (no initializer): new TResult(ctx.Key, ctx.Avg(...)) + ArgumentListSyntax? argList = body switch + { + ObjectCreationExpressionSyntax { Initializer: null } oc => oc.ArgumentList, + ImplicitObjectCreationExpressionSyntax { Initializer: null } ic => ic.ArgumentList, + _ => null, + }; + + if (argList is not null) + foreach (var arg in argList.Arguments) + results.Add(arg.Expression); + + return results; + } + + private static bool IsValidAggregateBindingValue( + ExpressionSyntax expr, + IParameterSymbol? ctxSymbol, + string ctxParamName, + SemanticModel semanticModel + ) + { + // ctx.Key — member access on the context parameter. + if (expr is MemberAccessExpressionSyntax ma) + return IsContextReceiver(ma.Expression, ctxSymbol, ctxParamName, semanticModel); + + // ctx.Avg(...) / ctx.Sum(...) / ctx.Count() — invocation on the context parameter. + if (expr is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax invMa }) + return IsContextReceiver(invMa.Expression, ctxSymbol, ctxParamName, semanticModel); + + return false; + } + + private static bool IsContextReceiver( + ExpressionSyntax receiver, + IParameterSymbol? ctxSymbol, + string ctxParamName, + SemanticModel semanticModel + ) + { + if (receiver is not IdentifierNameSyntax id || id.Identifier.Text != ctxParamName) + return false; + + // Prefer symbol identity when available; fall back to name-only check. + if (ctxSymbol is not null) + { + var symbol = semanticModel.GetSymbolInfo(receiver).Symbol; + return SymbolEqualityComparer.Default.Equals(symbol, ctxSymbol); + } + + return true; + } +} diff --git a/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/TypedFrameInvocationHelper.cs b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/TypedFrameInvocationHelper.cs new file mode 100644 index 00000000..cfeafd48 --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames.SourceGenerators/TypedFrameInvocationHelper.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Flowthru.Misc.DataFrames.Analyzers; + +/// +/// Sentinel returned when an invocation is identified as targeting +/// TypedFrameExtensions or GroupedFrameExtensions. +/// +public sealed class TypedFrameInvocation +{ + /// The resolved method symbol. + public IMethodSymbol Method { get; } + + /// The full invocation expression syntax node. + public InvocationExpressionSyntax Invocation { get; } + + /// + /// The lambda argument syntax nodes, in argument-list order. Most operations + /// have one lambda; Join has three; terminal ops (Count, Distinct) have zero. + /// + public IReadOnlyList LambdaArguments { get; } + + internal TypedFrameInvocation( + IMethodSymbol method, + InvocationExpressionSyntax invocation, + IReadOnlyList lambdaArguments + ) + { + Method = method; + Invocation = invocation; + LambdaArguments = lambdaArguments; + } +} + +/// +/// Identifies invocations of TypedFrameExtensions and GroupedFrameExtensions +/// methods and extracts their lambda arguments for downstream analysis. +/// +public static class TypedFrameInvocationHelper +{ + private const string TypedFrameExtensionsName = "TypedFrameExtensions"; + private const string GroupedFrameExtensionsName = "GroupedFrameExtensions"; + + /// + /// Attempts to recognise as a call to a + /// TypedFrameExtensions or GroupedFrameExtensions method. + /// Returns null if it is not such a call. + /// + public static TypedFrameInvocation? TryMatch( + InvocationExpressionSyntax invocation, + SemanticModel semanticModel + ) + { + if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol methodSymbol) + return null; + + var containingType = methodSymbol.ContainingType?.Name; + if (containingType != TypedFrameExtensionsName && containingType != GroupedFrameExtensionsName) + return null; + + // Extract all lambda arguments from the argument list. + var lambdas = new List(); + foreach (var argument in invocation.ArgumentList.Arguments) + { + if (argument.Expression is LambdaExpressionSyntax lambda) + lambdas.Add(lambda); + } + + return new TypedFrameInvocation(methodSymbol, invocation, lambdas); + } +} diff --git a/src/misc/Flowthru.Misc.DataFrames/Flowthru.Misc.DataFrames.csproj b/src/misc/Flowthru.Misc.DataFrames/Flowthru.Misc.DataFrames.csproj new file mode 100644 index 00000000..d76a3396 --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames/Flowthru.Misc.DataFrames.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + Flowthru.Misc.DataFrames + Framework-agnostic typed DataFrame abstraction for Flowthru, based on the IQueryable provider pattern + flowthru;dataframe;typed;queryable + + + Flowthru.Misc.DataFrames + + + + + + + + + + + + + + + diff --git a/src/misc/Flowthru.Misc.DataFrames/FrameExpressionVisitor.cs b/src/misc/Flowthru.Misc.DataFrames/FrameExpressionVisitor.cs new file mode 100644 index 00000000..2612fa13 --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames/FrameExpressionVisitor.cs @@ -0,0 +1,216 @@ +using System.Linq.Expressions; +using System.Reflection; +using Flowthru.Core.Abstractions; + +namespace Flowthru.Misc.DataFrames; + +/// +/// Base class for translating expression trees into native +/// frame operations. +/// +/// +/// +/// This class mirrors the role of EF Core's QueryableMethodTranslatingExpressionVisitor. +/// It walks the expression tree built by methods and +/// dispatches to abstract handler methods that providers implement for their native backend. +/// +/// +/// Providers subclass this and implement the Translate* methods to emit native +/// operations (e.g., Spark Column expressions, ML.NET IEstimator chains). +/// +/// +public abstract class FrameExpressionVisitor +{ + /// + /// Compiles a full expression tree into a native frame object. + /// + /// + /// The expression tree is rooted at a constant (a leaf + /// backed by a native frame), with chained nodes + /// representing operations like Where, Select, and Join. + /// + public object CompileExpression(Expression expression) + { + return expression switch + { + MethodCallExpression mce => TranslateMethodCall(mce), + ConstantExpression ce => TranslateConstant(ce), + _ => throw new NotSupportedException( + $"Expression node type '{expression.NodeType}' is not supported at the top level. " + + "Only method call chains rooted at a TypedFrame constant are supported." + ), + }; + } + + /// + /// Dispatches a method call to the appropriate handler, or throws if unrecognized. + /// + protected virtual object TranslateMethodCall(MethodCallExpression node) + { + if (node.Method.DeclaringType == typeof(TypedFrameExtensions)) + { + return node.Method.Name switch + { + nameof(TypedFrameExtensions.Where) => TranslateWhere(node), + nameof(TypedFrameExtensions.Select) => TranslateSelect(node), + nameof(TypedFrameExtensions.Join) => TranslateJoin(node), + nameof(TypedFrameExtensions.OrderBy) => TranslateOrderBy(node, descending: false), + nameof(TypedFrameExtensions.OrderByDescending) => TranslateOrderBy(node, descending: true), + nameof(TypedFrameExtensions.Take) => TranslateTake(node), + nameof(TypedFrameExtensions.Count) => TranslateCount(node), + nameof(TypedFrameExtensions.GroupBy) => TranslateGroupBy(node), + nameof(TypedFrameExtensions.Distinct) => TranslateDistinct(node), + nameof(TypedFrameExtensions.Union) => TranslateUnion(node), + nameof(TypedFrameExtensions.SelectOver) => TranslateSelectOver(node), + _ => throw new NotSupportedException( + $"TypedFrame operation '{node.Method.Name}' is not yet supported." + ), + }; + } + + if (node.Method.DeclaringType == typeof(GroupedFrameExtensions)) + { + return node.Method.Name switch + { + nameof(GroupedFrameExtensions.Aggregate) => TranslateAggregate(node), + _ => throw new NotSupportedException( + $"GroupedFrame operation '{node.Method.Name}' is not yet supported." + ), + }; + } + + throw new NotSupportedException( + $"Method '{node.Method.DeclaringType?.Name}.{node.Method.Name}' is not a recognized " + + "TypedFrame operation. Only methods defined on TypedFrameExtensions or GroupedFrameExtensions are supported." + ); + } + + /// + /// Translates a wrapping a root + /// into the native frame it represents. + /// + protected abstract object TranslateConstant(ConstantExpression node); + + /// + /// Translates a Where operation into a native filter. + /// + /// + /// Method call with arguments: [0] source expression, [1] quoted predicate lambda. + /// + protected abstract object TranslateWhere(MethodCallExpression node); + + /// + /// Translates a Select operation into a native projection. + /// + /// + /// Method call with arguments: [0] source expression, [1] quoted selector lambda. + /// + protected abstract object TranslateSelect(MethodCallExpression node); + + /// + /// Translates a Join operation into a native join + projection. + /// + /// + /// Method call with arguments: [0] outer source, [1] inner source, + /// [2] outer key selector, [3] inner key selector, [4] result selector. + /// + protected abstract object TranslateJoin(MethodCallExpression node); + + /// + /// Translates an OrderBy or OrderByDescending operation into a native sort. + /// + /// Method call with arguments: [0] source, [1] quoted key selector. + /// True for descending order; false for ascending. + protected abstract object TranslateOrderBy(MethodCallExpression node, bool descending); + + /// + /// Translates a Take operation into a native row limit. + /// + /// Method call with arguments: [0] source, [1] count constant. + protected abstract object TranslateTake(MethodCallExpression node); + + /// + /// Translates a Count operation into a scalar row count. + /// + /// Method call with arguments: [0] source. + protected abstract object TranslateCount(MethodCallExpression node); + + /// + /// Translates a Distinct operation into a native deduplication. + /// + /// Method call with arguments: [0] source. + protected abstract object TranslateDistinct(MethodCallExpression node); + + /// + /// Translates a Union operation into a native row-wise concatenation. + /// + /// Method call with arguments: [0] source, [1] other source. + protected abstract object TranslateUnion(MethodCallExpression node); + + /// + /// Translates a GroupBy operation into a native grouped dataset. + /// + /// Method call with arguments: [0] source, [1] quoted key selector. + protected abstract object TranslateGroupBy(MethodCallExpression node); + + /// + /// Translates an Aggregate operation on a grouped frame into a native aggregation. + /// + /// + /// Method call with arguments: [0] grouped source expression, [1] quoted result selector. + /// The result selector's body is a MemberInitExpression or NewExpression + /// whose bindings reference methods. + /// + protected abstract object TranslateAggregate(MethodCallExpression node); + + /// + /// Translates a SelectOver operation into a native windowed projection. + /// + /// + /// Method call with arguments: [0] source expression, + /// [1] quoted two-parameter selector (TSource, WindowContext<TSource>) => TResult. + /// Bindings referencing the WindowContext parameter carry + /// arguments that describe the window. + /// + protected abstract object TranslateSelectOver(MethodCallExpression node); + + // ────────────────────────────────────────────── + // Shared helpers + // ────────────────────────────────────────────── + + /// + /// Resolves the external column name for a schema property, respecting + /// if present. + /// + protected static string ResolveColumnName(MemberInfo member) + { + var attr = member.GetCustomAttribute(); + return attr?.Label ?? member.Name; + } + + /// + /// Extracts the from a quoted argument. + /// + protected static LambdaExpression Unquote(Expression expression) + { + if (expression is UnaryExpression { NodeType: ExpressionType.Quote } unary) + return (LambdaExpression)unary.Operand; + + if (expression is LambdaExpression lambda) + return lambda; + + throw new InvalidOperationException( + $"Expected a quoted lambda expression, got {expression.NodeType}." + ); + } + + /// + /// Evaluates an expression that doesn't reference any DataFrame columns + /// (e.g., a closure-captured variable or a static field) to its runtime value. + /// + protected static object? EvaluateConstant(Expression expression) + { + var lambda = Expression.Lambda>(Expression.Convert(expression, typeof(object))); + return lambda.Compile().Invoke(); + } +} diff --git a/src/misc/Flowthru.Misc.DataFrames/FrameWindowSpec.cs b/src/misc/Flowthru.Misc.DataFrames/FrameWindowSpec.cs new file mode 100644 index 00000000..a9a93db6 --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames/FrameWindowSpec.cs @@ -0,0 +1,112 @@ +using System.Linq.Expressions; + +namespace Flowthru.Misc.DataFrames; + +/// +/// Non-generic contract for a window specification, used by visitors to translate +/// window definitions without requiring the generic source type parameter. +/// +public interface IFrameWindowSpec +{ + /// Partition-by expressions, in the order they were added. + IReadOnlyList PartitionByExpressions { get; } + + /// Order-by expressions, each paired with a descending flag. + IReadOnlyList<(LambdaExpression KeySelector, bool Descending)> OrderByExpressions { get; } +} + +/// +/// An immutable, framework-agnostic window specification that describes how rows are +/// partitioned and ordered for windowed computations. +/// +/// +/// +/// FrameWindowSpec<TSource> is a pure data carrier — it holds +/// trees for partition and order keys. No native +/// (Spark, SQL, etc.) objects are created until the provider's expression visitor +/// translates the spec at query compilation time. +/// +/// +/// Build specs with the static or +/// entry points and the fluent instance methods. +/// Pass the finished spec as the last argument to each +/// function call inside a +/// projection. +/// +/// +/// The row schema type the spec applies to. +public sealed class FrameWindowSpec : IFrameWindowSpec +{ + private FrameWindowSpec( + IReadOnlyList partitionBy, + IReadOnlyList<(LambdaExpression KeySelector, bool Descending)> orderBy + ) + { + PartitionByExpressions = partitionBy; + OrderByExpressions = orderBy; + } + + /// + /// An empty window spanning all rows with no partition or ordering. + /// Use as the starting point when only ordering is needed: + /// FrameWindowSpec<T>.Global.OrderBy(x => x.HireDate). + /// + public static readonly FrameWindowSpec Global = new([], []); + + /// + public IReadOnlyList PartitionByExpressions { get; } + + /// + public IReadOnlyList<(LambdaExpression KeySelector, bool Descending)> OrderByExpressions { get; } + + // ────────────────────────────────────────────── + // Static entry points + // ────────────────────────────────────────────── + + /// + /// Creates a new spec with a single partition key. + /// + public static FrameWindowSpec PartitionBy( + Expression> keySelector + ) + { + ArgumentNullException.ThrowIfNull(keySelector); + return new FrameWindowSpec([keySelector], []); + } + + // ────────────────────────────────────────────── + // Fluent instance methods + // ────────────────────────────────────────────── + + /// Adds an additional partition key to this spec. + public FrameWindowSpec ThenPartitionBy(Expression> keySelector) + { + ArgumentNullException.ThrowIfNull(keySelector); + return new FrameWindowSpec( + [.. PartitionByExpressions, keySelector], + OrderByExpressions + ); + } + + /// Adds an ascending sort key to this spec. + public FrameWindowSpec OrderBy(Expression> keySelector) + { + ArgumentNullException.ThrowIfNull(keySelector); + return new FrameWindowSpec( + PartitionByExpressions, + [.. OrderByExpressions, (keySelector, false)] + ); + } + + /// Adds a descending sort key to this spec. + public FrameWindowSpec OrderByDescending( + Expression> keySelector + ) + { + ArgumentNullException.ThrowIfNull(keySelector); + return new FrameWindowSpec( + PartitionByExpressions, + [.. OrderByExpressions, (keySelector, true)] + ); + } +} diff --git a/src/misc/Flowthru.Misc.DataFrames/GroupedFrame.cs b/src/misc/Flowthru.Misc.DataFrames/GroupedFrame.cs new file mode 100644 index 00000000..b573f6a2 --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames/GroupedFrame.cs @@ -0,0 +1,125 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace Flowthru.Misc.DataFrames; + +/// +/// An intermediate representation of a grouped , produced by +/// . +/// +/// +/// This type exists solely as a typed anchor for the subsequent +/// call. It carries the accumulated group expression and prevents accidental misuse +/// of a grouped frame as a regular frame. +/// +/// The type of the grouping key. +/// The row schema type before grouping. +public sealed class GroupedFrame +{ + internal IFrameQueryProvider Provider { get; } + public Expression Expression { get; } + + internal GroupedFrame(IFrameQueryProvider provider, Expression expression) + { + Provider = provider; + Expression = expression; + } +} + +/// +/// Extension methods for . +/// +public static class GroupedFrameExtensions +{ + /// + /// Aggregates a grouped frame, producing a new . + /// + /// The grouping key type. + /// The source row schema type. + /// The result schema type after aggregation. + /// The grouped frame. + /// + /// A projection from a to the result schema. + /// The context exposes typed aggregate functions (Avg, Sum, Count, Min, Max) and the key. + /// + public static TypedFrame Aggregate( + this GroupedFrame source, + Expression, TResult>> resultSelector + ) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(resultSelector); + + var method = ( + (Func< + GroupedFrame, + Expression, TResult>>, + TypedFrame + >) + Aggregate + ).Method; + + return (TypedFrame) + source.Provider.CreateQuery( + Expression.Call(null, method, source.Expression, Expression.Quote(resultSelector)) + ); + } +} + +/// +/// Provides typed aggregate function placeholders within an +/// expression. +/// +/// +/// Instances of this type are never constructed at runtime. The expression visitor +/// intercepts calls to its methods during expression tree translation and maps them +/// to the corresponding native aggregate functions (e.g., Spark's avg()). +/// +/// The grouping key type. +/// The source row schema type. +public sealed class AggregationContext +{ + private AggregationContext() { } + + /// The grouping key value for this group. + public TKey Key => throw new InvalidOperationException(AggregationContextError); + + /// Computes the average of a numeric column. + public double Avg(Expression> column) => + throw new InvalidOperationException(AggregationContextError); + + /// Computes the average of a numeric column. + public decimal Avg(Expression> column) => + throw new InvalidOperationException(AggregationContextError); + + /// Computes the average of a numeric column. + public double Avg(Expression> column) => + throw new InvalidOperationException(AggregationContextError); + + /// Computes the sum of a numeric column. + public double Sum(Expression> column) => + throw new InvalidOperationException(AggregationContextError); + + /// Computes the sum of a numeric column. + public decimal Sum(Expression> column) => + throw new InvalidOperationException(AggregationContextError); + + /// Computes the sum of a numeric column. + public long Sum(Expression> column) => + throw new InvalidOperationException(AggregationContextError); + + /// Computes the maximum value of a column. + public TValue Max(Expression> column) => + throw new InvalidOperationException(AggregationContextError); + + /// Computes the minimum value of a column. + public TValue Min(Expression> column) => + throw new InvalidOperationException(AggregationContextError); + + /// Counts the number of rows in the group. + public long Count() => throw new InvalidOperationException(AggregationContextError); + + private const string AggregationContextError = + "AggregationContext methods are expression tree placeholders and must not be invoked directly. " + + "They are translated to native aggregate functions by the provider's expression visitor."; +} diff --git a/src/misc/Flowthru.Misc.DataFrames/IFrameQueryProvider.cs b/src/misc/Flowthru.Misc.DataFrames/IFrameQueryProvider.cs new file mode 100644 index 00000000..fb8a49f6 --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames/IFrameQueryProvider.cs @@ -0,0 +1,36 @@ +using System.Linq.Expressions; + +namespace Flowthru.Misc.DataFrames; + +/// +/// A query provider that creates instances and compiles +/// accumulated expression trees into native frame operations. +/// +/// +/// This interface extends with a +/// method for producing native frame objects (e.g., a Spark +/// DataFrame) from the expression tree accumulated by chained operations. +/// Each provider implementation handles a specific DataFrame backend. +/// +public interface IFrameQueryProvider : IQueryProvider +{ + /// + /// Compiles the accumulated expression tree into a native frame object. + /// + /// + /// The expression tree rooted at a constant, + /// with chained method calls representing operations. + /// + /// The native frame object (e.g., Spark DataFrame). + object Compile(Expression expression); + + /// + /// Materializes the accumulated expression tree into an enumerable sequence of rows. + /// Called by to enable transparent + /// TypedFrame → IEnumerable conversion at catalog item boundaries. + /// + /// The row schema type. + /// The accumulated expression tree. + /// The materialized rows. + IEnumerable Materialize(Expression expression); +} diff --git a/src/misc/Flowthru.Misc.DataFrames/IFrameTranslatorPlugin.cs b/src/misc/Flowthru.Misc.DataFrames/IFrameTranslatorPlugin.cs new file mode 100644 index 00000000..15139686 --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames/IFrameTranslatorPlugin.cs @@ -0,0 +1,46 @@ +using System.Reflection; + +namespace Flowthru.Misc.DataFrames; + +/// +/// Translates .NET member access (property or field) into a native column expression. +/// +/// +/// Providers register implementations to teach the expression visitor how to handle +/// property reads beyond direct schema properties — for example, translating +/// string.Length into a native string-length function. +/// +public interface IFrameMemberTranslator +{ + /// + /// Attempts to translate a member access into a native expression. + /// + /// The property or field being accessed. + /// + /// The translated native expression for the instance, or null for static members. + /// + /// A native expression, or null if this translator does not handle the member. + object? Translate(MemberInfo member, object? instance); +} + +/// +/// Translates .NET method calls into native frame operations. +/// +/// +/// Providers register implementations to teach the expression visitor how to handle +/// method calls — for example, translating Math.Abs(x) into a native +/// absolute-value function. +/// +public interface IFrameMethodTranslator +{ + /// + /// Attempts to translate a method call into a native expression. + /// + /// The method being called. + /// + /// The translated native expression for the instance, or null for static methods. + /// + /// The translated native expressions for each argument. + /// A native expression, or null if this translator does not handle the method. + object? Translate(MethodInfo method, object? instance, IReadOnlyList arguments); +} diff --git a/src/misc/Flowthru.Misc.DataFrames/TypedFrame.cs b/src/misc/Flowthru.Misc.DataFrames/TypedFrame.cs new file mode 100644 index 00000000..5d804887 --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames/TypedFrame.cs @@ -0,0 +1,76 @@ +using System.Collections; +using System.Linq.Expressions; + +namespace Flowthru.Misc.DataFrames; + +/// +/// A phantom-typed wrapper around an untyped DataFrame-like object. +/// +/// +/// +/// TypedFrame<T> implements to leverage the standard +/// .NET expression tree infrastructure. The type parameter is a +/// phantom type — it carries schema information through the type system without being +/// instantiated at runtime. +/// +/// +/// Extension methods build expression trees via , +/// threading type parameters through each operation (just as LINQ's Queryable methods do). +/// When the accumulated expression tree is compiled by the provider, it produces native +/// DataFrame operations (e.g., Spark Column expressions, ML.NET transforms) without +/// materializing data into .NET objects. +/// +/// +/// +/// The schema type representing the row structure. Must be annotated with +/// [FlowthruSchema] to participate in compile-time and pre-flight validation. +/// +public class TypedFrame : IQueryable, IOrderedQueryable +{ + private readonly IFrameQueryProvider _provider; + private readonly Expression _expression; + + /// + /// Creates a root frame node backed by a native DataFrame. + /// The provider associates the native frame externally. + /// + public TypedFrame(IFrameQueryProvider provider) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _expression = Expression.Constant(this); + } + + /// + /// Creates an intermediate frame node representing an accumulated operation. + /// Used by the provider's . + /// + public TypedFrame(IFrameQueryProvider provider, Expression expression) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _expression = expression ?? throw new ArgumentNullException(nameof(expression)); + } + + /// + public Expression Expression => _expression; + + /// + public Type ElementType => typeof(T); + + /// + public IQueryProvider Provider => _provider; + + /// + /// Materializes this frame by delegating to the provider's + /// method. + /// + /// + /// This enables transparent TypedFrame → IEnumerable conversion at catalog item + /// boundaries: a step returning TypedFrame<T> can be wired to a + /// catalog item typed as IEnumerable<T> without any explicit + /// materialization call in step code. + /// + public IEnumerator GetEnumerator() => _provider.Materialize(_expression).GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/misc/Flowthru.Misc.DataFrames/TypedFrameExtensions.cs b/src/misc/Flowthru.Misc.DataFrames/TypedFrameExtensions.cs new file mode 100644 index 00000000..bb03b32a --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames/TypedFrameExtensions.cs @@ -0,0 +1,317 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace Flowthru.Misc.DataFrames; + +/// +/// LINQ-style extension methods for that build expression trees. +/// +/// +/// These methods follow the same pattern as : each call +/// captures the lambda as an tree node and delegates to +/// . No native operations execute here — +/// translation is deferred to the provider's method. +/// +public static class TypedFrameExtensions +{ + // ────────────────────────────────────────────── + // Where — type-preserving filter + // ────────────────────────────────────────────── + + /// + /// Filters rows using a predicate. The schema type is preserved. + /// + public static TypedFrame Where( + this TypedFrame source, + Expression> predicate + ) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(predicate); + + return (TypedFrame) + source.Provider.CreateQuery( + Expression.Call( + null, + CaptureMethod(Where, source, predicate), + source.Expression, + Expression.Quote(predicate) + ) + ); + } + + // ────────────────────────────────────────────── + // Select — type-projecting transformation + // ────────────────────────────────────────────── + + /// + /// Projects each row into a new schema type via a selector expression. + /// + public static TypedFrame Select( + this TypedFrame source, + Expression> selector + ) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(selector); + + return (TypedFrame) + source.Provider.CreateQuery( + Expression.Call( + null, + CaptureMethod(Select, source, selector), + source.Expression, + Expression.Quote(selector) + ) + ); + } + + // ────────────────────────────────────────────── + // Join — multi-frame equi-join with projection + // ────────────────────────────────────────────── + + /// + /// Joins two typed frames on matching keys and projects the result into a new schema. + /// + public static TypedFrame Join( + this TypedFrame outer, + TypedFrame inner, + Expression> outerKeySelector, + Expression> innerKeySelector, + Expression> resultSelector + ) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + ArgumentNullException.ThrowIfNull(resultSelector); + + return (TypedFrame) + outer.Provider.CreateQuery( + Expression.Call( + null, + CaptureMethod(Join, outer, inner, outerKeySelector, innerKeySelector, resultSelector), + outer.Expression, + inner.Expression, + Expression.Quote(outerKeySelector), + Expression.Quote(innerKeySelector), + Expression.Quote(resultSelector) + ) + ); + } + + // ────────────────────────────────────────────── + // OrderBy / OrderByDescending — type-preserving sort + // ────────────────────────────────────────────── + + /// + /// Sorts rows by a key in ascending order. The schema type is preserved. + /// + public static TypedFrame OrderBy( + this TypedFrame source, + Expression> keySelector + ) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(keySelector); + + return (TypedFrame) + source.Provider.CreateQuery( + Expression.Call( + null, + CaptureMethod(OrderBy, source, keySelector), + source.Expression, + Expression.Quote(keySelector) + ) + ); + } + + /// + /// Sorts rows by a key in descending order. The schema type is preserved. + /// + public static TypedFrame OrderByDescending( + this TypedFrame source, + Expression> keySelector + ) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(keySelector); + + return (TypedFrame) + source.Provider.CreateQuery( + Expression.Call( + null, + CaptureMethod(OrderByDescending, source, keySelector), + source.Expression, + Expression.Quote(keySelector) + ) + ); + } + + // ────────────────────────────────────────────── + // Take — type-preserving row limit + // ────────────────────────────────────────────── + + /// + /// Limits the frame to the first rows. The schema type is preserved. + /// + public static TypedFrame Take(this TypedFrame source, int count) + { + ArgumentNullException.ThrowIfNull(source); + + return (TypedFrame) + source.Provider.CreateQuery( + Expression.Call( + null, + CaptureMethod(Take, source, count), + source.Expression, + Expression.Constant(count) + ) + ); + } + + // ────────────────────────────────────────────── + // Count — scalar execution + // ────────────────────────────────────────────── + + /// + /// Returns the number of rows in the frame. + /// + /// + /// This triggers compilation and execution via the provider. It is a terminal operation. + /// + public static long Count(this TypedFrame source) + { + ArgumentNullException.ThrowIfNull(source); + + var expression = Expression.Call(null, CaptureMethod(Count, source), source.Expression); + + return source.Provider.Execute(expression); + } + + // ────────────────────────────────────────────── + // Distinct — deduplicate rows + // ────────────────────────────────────────────── + + /// + /// Returns a frame with duplicate rows removed. + /// + public static TypedFrame Distinct(this TypedFrame source) + { + ArgumentNullException.ThrowIfNull(source); + + return (TypedFrame) + source.Provider.CreateQuery( + Expression.Call(null, CaptureMethod(Distinct, source), source.Expression) + ); + } + + // ────────────────────────────────────────────── + // Union — row-wise concatenation + // ────────────────────────────────────────────── + + /// + /// Concatenates two frames of the same schema, preserving all rows (including duplicates). + /// Equivalent to SQL UNION ALL; use after to get + /// distinct-row semantics. + /// + public static TypedFrame Union( + this TypedFrame source, + TypedFrame other + ) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(other); + + return (TypedFrame) + source.Provider.CreateQuery( + Expression.Call( + null, + CaptureMethod(Union, source, other), + source.Expression, + other.Expression + ) + ); + } + + // ────────────────────────────────────────────── + // GroupBy — intermediate grouped frame + // ────────────────────────────────────────────── + + /// + /// Groups rows by a key selector, producing a + /// that can be aggregated via . + /// + public static GroupedFrame GroupBy( + this TypedFrame source, + Expression> keySelector + ) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(keySelector); + + var expression = Expression.Call( + null, + CaptureMethod(GroupBy, source, keySelector), + source.Expression, + Expression.Quote(keySelector) + ); + + return new GroupedFrame((IFrameQueryProvider)source.Provider, expression); + } + + // ────────────────────────────────────────────── + // SelectOver — windowed projection + // ────────────────────────────────────────────── + + /// + /// Projects each row into a new schema type, with access to windowed aggregate and + /// ranking functions via the parameter. + /// + /// + /// Each window function call in the selector must pass a + /// as its last argument, which defines the + /// partition and ordering for that specific function. Multiple specs may appear in + /// the same projection, enabling multi-window queries in a single call. + /// + public static TypedFrame SelectOver( + this TypedFrame source, + Expression, TResult>> selector + ) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(selector); + + return (TypedFrame) + source.Provider.CreateQuery( + Expression.Call( + null, + CaptureMethod(SelectOver, source, selector), + source.Expression, + Expression.Quote(selector) + ) + ); + } + + // ────────────────────────────────────────────── + // MethodInfo capture helpers + // ────────────────────────────────────────────── + // These follow the same pattern as System.Linq.Queryable's GetMethodInfo: + // the dummy parameters exist solely for generic type inference so the + // compiler resolves the closed generic MethodInfo at the call site. + + private static MethodInfo CaptureMethod(Func method, T1 _1) => method.Method; + + private static MethodInfo CaptureMethod(Func method, T1 _1, T2 _2) => + method.Method; + + private static MethodInfo CaptureMethod( + Func method, + T1 _1, + T2 _2, + T3 _3, + T4 _4, + T5 _5 + ) => method.Method; +} diff --git a/src/misc/Flowthru.Misc.DataFrames/WindowContext.cs b/src/misc/Flowthru.Misc.DataFrames/WindowContext.cs new file mode 100644 index 00000000..c858b9da --- /dev/null +++ b/src/misc/Flowthru.Misc.DataFrames/WindowContext.cs @@ -0,0 +1,116 @@ +using System.Linq.Expressions; + +namespace Flowthru.Misc.DataFrames; + +/// +/// A throw-only marker type whose methods are intercepted as expression tree nodes +/// inside a projection. +/// +/// +/// +/// Instances of this type are never constructed at runtime. The provider's expression +/// visitor recognises method calls on the win parameter and translates them to +/// the corresponding native window functions (e.g., Spark's +/// Functions.Rank().Over(windowSpec)). +/// +/// +/// Each method accepts a as its last argument. +/// This makes multi-window projections natural — different columns can reference different +/// specs in the same SelectOver call. +/// +/// +/// The row schema type of the source frame. +public sealed class WindowContext +{ + private WindowContext() { } + + private const string Error = + "WindowContext methods are expression tree placeholders and must not be invoked directly. " + + "They are translated to native window functions by the provider's expression visitor."; + + // ────────────────────────────────────────────── + // Ranking functions (no column selector) + // ────────────────────────────────────────────── + + /// Sequential row number within the partition, starting at 1. + public long RowNumber(FrameWindowSpec spec) => + throw new InvalidOperationException(Error); + + /// Rank with gaps (ties share a rank; the next rank reflects the gap). + public long Rank(FrameWindowSpec spec) => throw new InvalidOperationException(Error); + + /// Rank without gaps (ties share a rank; next rank is always rank + 1). + public long DenseRank(FrameWindowSpec spec) => + throw new InvalidOperationException(Error); + + /// Fraction of rows within the partition that are less than or equal to the current row. + public double CumeDist(FrameWindowSpec spec) => + throw new InvalidOperationException(Error); + + /// Relative rank of the current row: (rank - 1) / (partition size - 1). + public double PercentRank(FrameWindowSpec spec) => + throw new InvalidOperationException(Error); + + /// Count of rows seen so far within the window frame. + public long Count(FrameWindowSpec spec) => throw new InvalidOperationException(Error); + + // ────────────────────────────────────────────── + // Offset functions (column selector + offset) + // ────────────────────────────────────────────── + + /// + /// Value of from the row rows before + /// the current row, or null if no such row exists. + /// + public TValue? Lag( + Expression> selector, + int offset, + FrameWindowSpec spec + ) => throw new InvalidOperationException(Error); + + /// + /// Value of from the row rows after + /// the current row, or null if no such row exists. + /// + public TValue? Lead( + Expression> selector, + int offset, + FrameWindowSpec spec + ) => throw new InvalidOperationException(Error); + + // ────────────────────────────────────────────── + // Aggregate window functions (column selector) + // ────────────────────────────────────────────── + + /// Running sum of over the window frame. + public double Sum(Expression> selector, FrameWindowSpec spec) => + throw new InvalidOperationException(Error); + + /// + public decimal Sum(Expression> selector, FrameWindowSpec spec) => + throw new InvalidOperationException(Error); + + /// + public long Sum(Expression> selector, FrameWindowSpec spec) => + throw new InvalidOperationException(Error); + + /// Running average of over the window frame. + public double Avg(Expression> selector, FrameWindowSpec spec) => + throw new InvalidOperationException(Error); + + /// + public double Avg(Expression> selector, FrameWindowSpec spec) => + throw new InvalidOperationException(Error); + + /// Running maximum of over the window frame. + public TValue Max( + Expression> selector, + FrameWindowSpec spec + ) => throw new InvalidOperationException(Error); + + /// Running minimum of over the window frame. + public TValue Min( + Expression> selector, + FrameWindowSpec spec + ) => throw new InvalidOperationException(Error); +} diff --git a/src/spark/Flowthru.Spark.Jvm/.gitignore b/src/spark/Flowthru.Spark.Jvm/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/.gitignore @@ -0,0 +1 @@ +target diff --git a/src/spark/Flowthru.Spark.Jvm/LICENSE-DOTNET-SPARK b/src/spark/Flowthru.Spark.Jvm/LICENSE-DOTNET-SPARK new file mode 100644 index 00000000..984713a4 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/LICENSE-DOTNET-SPARK @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/spark/Flowthru.Spark.Jvm/README.md b/src/spark/Flowthru.Spark.Jvm/README.md new file mode 100644 index 00000000..56e0c72a --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/README.md @@ -0,0 +1,3 @@ +# Flowthru.Spark.Jvm + +The JVM project is a fork of Spark.NET's Scala code, responsible for writing the bridge JAR that connects the .NET library to Spark's interface. diff --git a/src/spark/Flowthru.Spark.Jvm/pom.xml b/src/spark/Flowthru.Spark.Jvm/pom.xml new file mode 100644 index 00000000..9ba502dd --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/pom.xml @@ -0,0 +1,79 @@ + + 4.0.0 + com.microsoft.scala + flowthru-spark-4-1_2.13 + 2.3.1 + 2019 + + UTF-8 + 2.13.17 + 2.13 + 4.1.1 + + + + + org.scala-lang + scala-library + ${scala.version} + + + org.apache.spark + spark-core_${scala.binary.version} + ${spark.version} + provided + + + org.apache.spark + spark-sql_${scala.binary.version} + ${spark.version} + provided + + + org.apache.spark + spark-mllib_${scala.binary.version} + ${spark.version} + provided + + + junit + junit + 4.13.1 + test + + + org.specs + specs + 1.2.5 + test + + + + + src/main/scala + src/test/scala + + + org.scala-tools + maven-scala-plugin + 2.15.2 + + + + compile + testCompile + + + + + ${scala.version} + + -deprecation + -feature + + + + + + diff --git a/src/spark/Flowthru.Spark.Jvm/project.json b/src/spark/Flowthru.Spark.Jvm/project.json new file mode 100644 index 00000000..0c51df91 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/project.json @@ -0,0 +1,40 @@ +{ + "name": "Flowthru.Spark.Jvm", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "src/spark/Flowthru.Spark.Jvm", + "tags": [ "lang:scala", "scope:spark" ], + "targets": { + "build": { + "//": "Compiles the Scala JVM bridge JAR, then moves it to dist/. -q and --no-transfer-progress suppress Maven info/download noise.", + "executor": "nx:run-commands", + "options": { + "commands": [ + "mkdir -p ../../../dist/src/spark/Flowthru.Spark.Jvm", + "mvn package -DskipTests -q --no-transfer-progress", + "mv ./target/flowthru-spark-4-1_2.13-2.3.1.jar ../../../dist/src/spark/Flowthru.Spark.Jvm/", + "mv ./target ../../../dist/src/spark/Flowthru.Spark.Jvm/target" + ], + "cwd": "src/spark/Flowthru.Spark.Jvm", + "parallel": false + }, + "cache": true, + "inputs": [ + "{projectRoot}/src/**/*.scala", + "{projectRoot}/pom.xml" + ], + "outputs": [ + "{workspaceRoot}/dist/src/spark/Flowthru.Spark.Jvm" + ] + }, + "clean": { + "//": "Removes the JAR from dist/", + "executor": "nx:run-commands", + "options": { + "commands": [ "rm -rf dist/src/spark/Flowthru.Spark.Jvm" ], + "cwd": "{workspaceRoot}", + "parallel": false + } + } + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/CallbackClient.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/CallbackClient.scala new file mode 100644 index 00000000..aea355df --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/CallbackClient.scala @@ -0,0 +1,72 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.api.dotnet + +import java.io.DataOutputStream + +import org.apache.spark.internal.Logging + +import scala.collection.mutable.Queue + +/** + * CallbackClient is used to communicate with the Dotnet CallbackServer. + * The client manages and maintains a pool of open CallbackConnections. + * Any callback request is delegated to a new CallbackConnection or + * unused CallbackConnection. + * @param address The address of the Dotnet CallbackServer + * @param port The port of the Dotnet CallbackServer + */ +class CallbackClient(serDe: SerDe, address: String, port: Int) extends Logging { + private[this] val connectionPool: Queue[CallbackConnection] = Queue[CallbackConnection]() + + private[this] var isShutdown: Boolean = false + + final def send(callbackId: Int, writeBody: (DataOutputStream, SerDe) => Unit): Unit = + getOrCreateConnection() match { + case Some(connection) => + try { + connection.send(callbackId, writeBody) + addConnection(connection) + } catch { + case e: Exception => + logError(s"Error calling callback [callback id = $callbackId].", e) + connection.close() + throw e + } + case None => throw new Exception("Unable to get or create connection.") + } + + private def getOrCreateConnection(): Option[CallbackConnection] = synchronized { + if (isShutdown) { + logInfo("Cannot get or create connection while client is shutdown.") + return None + } + + if (connectionPool.nonEmpty) { + return Some(connectionPool.dequeue()) + } + + Some(new CallbackConnection(serDe, address, port)) + } + + private def addConnection(connection: CallbackConnection): Unit = synchronized { + assert(connection != null) + connectionPool.enqueue(connection) + } + + def shutdown(): Unit = synchronized { + if (isShutdown) { + logInfo("Shutdown called, but already shutdown.") + return + } + + logInfo("Shutting down.") + connectionPool.foreach(_.close) + connectionPool.clear + isShutdown = true + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/CallbackConnection.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/CallbackConnection.scala new file mode 100644 index 00000000..604cf029 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/CallbackConnection.scala @@ -0,0 +1,112 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.api.dotnet + +import java.io.{ByteArrayOutputStream, Closeable, DataInputStream, DataOutputStream} +import java.net.Socket + +import org.apache.spark.internal.Logging + +/** + * CallbackConnection is used to process the callback communication + * between the JVM and Dotnet. It uses a TCP socket to communicate with + * the Dotnet CallbackServer and the socket is expected to be reused. + * @param address The address of the Dotnet CallbackServer + * @param port The port of the Dotnet CallbackServer + */ +class CallbackConnection(serDe: SerDe, address: String, port: Int) extends Logging { + private[this] val socket: Socket = new Socket(address, port) + private[this] val inputStream: DataInputStream = new DataInputStream(socket.getInputStream) + private[this] val outputStream: DataOutputStream = new DataOutputStream(socket.getOutputStream) + + def send( + callbackId: Int, + writeBody: (DataOutputStream, SerDe) => Unit): Unit = { + logInfo(s"Calling callback [callback id = $callbackId] ...") + + try { + serDe.writeInt(outputStream, CallbackFlags.CALLBACK) + serDe.writeInt(outputStream, callbackId) + + val byteArrayOutputStream = new ByteArrayOutputStream() + writeBody(new DataOutputStream(byteArrayOutputStream), serDe) + serDe.writeInt(outputStream, byteArrayOutputStream.size) + byteArrayOutputStream.writeTo(outputStream); + } catch { + case e: Exception => { + throw new Exception("Error writing to stream.", e) + } + } + + logInfo(s"Signaling END_OF_STREAM.") + try { + serDe.writeInt(outputStream, CallbackFlags.END_OF_STREAM) + outputStream.flush() + + val endOfStreamResponse = readFlag(inputStream) + endOfStreamResponse match { + case CallbackFlags.END_OF_STREAM => + logInfo(s"Received END_OF_STREAM signal. Calling callback [callback id = $callbackId] successful.") + case _ => { + throw new Exception(s"Error verifying end of stream. Expected: ${CallbackFlags.END_OF_STREAM}, " + + s"Received: $endOfStreamResponse") + } + } + } catch { + case e: Exception => { + throw new Exception("Error while verifying end of stream.", e) + } + } + } + + def close(): Unit = { + try { + serDe.writeInt(outputStream, CallbackFlags.CLOSE) + outputStream.flush() + } catch { + case e: Exception => logInfo("Unable to send close to .NET callback server.", e) + } + + close(socket) + close(outputStream) + close(inputStream) + } + + private def close(s: Socket): Unit = { + try { + assert(s != null) + s.close() + } catch { + case e: Exception => logInfo("Unable to close socket.", e) + } + } + + private def close(c: Closeable): Unit = { + try { + assert(c != null) + c.close() + } catch { + case e: Exception => logInfo("Unable to close closeable.", e) + } + } + + private def readFlag(inputStream: DataInputStream): Int = { + val callbackFlag = serDe.readInt(inputStream) + if (callbackFlag == CallbackFlags.DOTNET_EXCEPTION_THROWN) { + val exceptionMessage = serDe.readString(inputStream) + throw new DotnetException(exceptionMessage) + } + callbackFlag + } + + private object CallbackFlags { + val CLOSE: Int = -1 + val CALLBACK: Int = -2 + val DOTNET_EXCEPTION_THROWN: Int = -3 + val END_OF_STREAM: Int = -4 + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetBackend.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetBackend.scala new file mode 100644 index 00000000..c6f528ae --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetBackend.scala @@ -0,0 +1,113 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.api.dotnet + +import java.net.InetSocketAddress +import java.util.concurrent.TimeUnit +import io.netty.bootstrap.ServerBootstrap +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioServerSocketChannel +import io.netty.channel.{ChannelFuture, ChannelInitializer, EventLoopGroup} +import io.netty.handler.codec.LengthFieldBasedFrameDecoder +import io.netty.handler.codec.bytes.{ByteArrayDecoder, ByteArrayEncoder} +import org.apache.spark.internal.Logging +import org.apache.spark.internal.config.dotnet.Dotnet.DOTNET_NUM_BACKEND_THREADS +import org.apache.spark.{SparkConf, SparkEnv} + +/** + * Netty server that invokes JVM calls based upon receiving messages from .NET. + * The implementation mirrors the RBackend. + * + */ +class DotnetBackend extends Logging { + self => // for accessing the this reference in inner class(ChannelInitializer) + private[this] var channelFuture: ChannelFuture = _ + private[this] var bootstrap: ServerBootstrap = _ + private[this] var bossGroup: EventLoopGroup = _ + private[this] val objectTracker = new JVMObjectTracker + + @volatile + private[dotnet] var callbackClient: Option[CallbackClient] = None + + def init(portNumber: Int): Int = { + val conf = Option(SparkEnv.get).map(_.conf).getOrElse(new SparkConf()) + val numBackendThreads = conf.get(DOTNET_NUM_BACKEND_THREADS) + logInfo(s"The number of DotnetBackend threads is set to $numBackendThreads.") + bossGroup = new NioEventLoopGroup(numBackendThreads) + val workerGroup = bossGroup + + bootstrap = new ServerBootstrap() + .group(bossGroup, workerGroup) + .channel(classOf[NioServerSocketChannel]) + + bootstrap.childHandler(new ChannelInitializer[SocketChannel]() { + def initChannel(ch: SocketChannel): Unit = { + ch.pipeline() + .addLast("encoder", new ByteArrayEncoder()) + .addLast( + "frameDecoder", + // maxFrameLength = 2G + // lengthFieldOffset = 0 + // lengthFieldLength = 4 + // lengthAdjustment = 0 + // initialBytesToStrip = 4, i.e. strip out the length field itself + new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4)) + .addLast("decoder", new ByteArrayDecoder()) + .addLast("handler", new DotnetBackendHandler(self, objectTracker)) + } + }) + + channelFuture = bootstrap.bind(new InetSocketAddress("localhost", portNumber)) + channelFuture.syncUninterruptibly() + channelFuture.channel().localAddress().asInstanceOf[InetSocketAddress].getPort + } + + private[dotnet] def setCallbackClient(address: String, port: Int): Unit = synchronized { + callbackClient = callbackClient match { + case Some(_) => throw new Exception("Callback client already set.") + case None => + logInfo(s"Connecting to a callback server at $address:$port") + Some(new CallbackClient(new SerDe(objectTracker), address, port)) + } + } + + private[dotnet] def shutdownCallbackClient(): Unit = synchronized { + callbackClient match { + case Some(client) => client.shutdown() + case None => logInfo("Callback server has already been shutdown.") + } + callbackClient = None + } + + def run(): Unit = { + channelFuture.channel.closeFuture().syncUninterruptibly() + } + + def close(): Unit = { + if (channelFuture != null) { + // close is a local operation and should finish within milliseconds; timeout just to be safe + channelFuture.channel().close().awaitUninterruptibly(10, TimeUnit.SECONDS) + channelFuture = null + } + if (bootstrap != null && bootstrap.config().group() != null) { + bootstrap.config().group().shutdownGracefully() + } + if (bootstrap != null && bootstrap.config().childGroup() != null) { + bootstrap.config().childGroup().shutdownGracefully() + } + bootstrap = null + + objectTracker.clear() + + // Send close to .NET callback server. + shutdownCallbackClient() + + // Shutdown the thread pool whose executors could still be running. + ThreadPool.shutdown() + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetBackendHandler.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetBackendHandler.scala new file mode 100644 index 00000000..2863e5b3 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetBackendHandler.scala @@ -0,0 +1,337 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.api.dotnet + +import io.netty.channel.{ChannelHandlerContext, SimpleChannelInboundHandler} +import org.apache.spark.internal.Logging +import org.apache.spark.util.Utils + +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, DataInputStream, DataOutputStream} +import scala.collection.mutable.HashMap +import scala.language.existentials + +/** + * Handler for DotnetBackend. + * This implementation is similar to RBackendHandler. + */ +class DotnetBackendHandler(server: DotnetBackend, objectsTracker: JVMObjectTracker) + extends SimpleChannelInboundHandler[Array[Byte]] + with Logging { + + private[this] val serDe = new SerDe(objectsTracker) + + override def channelRead0(ctx: ChannelHandlerContext, msg: Array[Byte]): Unit = { + val reply = handleBackendRequest(msg) + ctx.write(reply) + } + + override def channelReadComplete(ctx: ChannelHandlerContext): Unit = { + ctx.flush() + } + + def handleBackendRequest(msg: Array[Byte]): Array[Byte] = { + val bis = new ByteArrayInputStream(msg) + val dis = new DataInputStream(bis) + + val bos = new ByteArrayOutputStream() + val dos = new DataOutputStream(bos) + + // First bit is isStatic + val isStatic = serDe.readBoolean(dis) + val processId = serDe.readInt(dis) + val threadId = serDe.readInt(dis) + val objId = serDe.readString(dis) + val methodName = serDe.readString(dis) + val numArgs = serDe.readInt(dis) + + if (objId == "DotnetHandler") { + methodName match { + case "stopBackend" => + serDe.writeInt(dos, 0) + serDe.writeType(dos, "void") + server.close() + case "rm" => + try { + val t = serDe.readObjectType(dis) + assert(t == 'c') + val objToRemove = serDe.readString(dis) + objectsTracker.remove(objToRemove) + serDe.writeInt(dos, 0) + serDe.writeObject(dos, null) + } catch { + case e: Exception => + logError(s"Removing $objId failed", e) + serDe.writeInt(dos, -1) + } + case "rmThread" => + try { + assert(serDe.readObjectType(dis) == 'i') + val processId = serDe.readInt(dis) + assert(serDe.readObjectType(dis) == 'i') + val threadToDelete = serDe.readInt(dis) + val result = ThreadPool.tryDeleteThread(processId, threadToDelete) + serDe.writeInt(dos, 0) + serDe.writeObject(dos, result.asInstanceOf[AnyRef]) + } catch { + case e: Exception => + logError(s"Removing thread $threadId failed", e) + serDe.writeInt(dos, -1) + } + case "connectCallback" => + assert(serDe.readObjectType(dis) == 'c') + val address = serDe.readString(dis) + assert(serDe.readObjectType(dis) == 'i') + val port = serDe.readInt(dis) + server.setCallbackClient(address, port) + serDe.writeInt(dos, 0) + + // Sends reference of CallbackClient to dotnet side, + // so that dotnet process can send the client back to Java side + // when calling any API containing callback functions. + serDe.writeObject(dos, server.callbackClient) + case "closeCallback" => + logInfo("Requesting to close callback client") + server.shutdownCallbackClient() + serDe.writeInt(dos, 0) + serDe.writeType(dos, "void") + case _ => dos.writeInt(-1) + } + } else { + ThreadPool + .run(processId, threadId, () => handleMethodCall(isStatic, objId, methodName, numArgs, dis, dos)) + } + + bos.toByteArray + } + + override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = { + // Skip logging the exception message if the connection was disconnected from + // the .NET side so that .NET side doesn't have to explicitly close the connection via + // "stopBackend." Note that an exception is still thrown if the exit status is non-zero, + // so skipping this kind of exception message does not affect the debugging. + if ( + !cause.getMessage.contains("An existing connection was forcibly closed by the remote host") + && !cause.getMessage.contains("Connection reset") + ) { + logError("Exception caught: ", cause) + } + + // Close the connection when an exception is raised. + ctx.close() + } + + def handleMethodCall( + isStatic: Boolean, + objId: String, + methodName: String, + numArgs: Int, + dis: DataInputStream, + dos: DataOutputStream): Unit = { + var obj: Object = null + var args: Array[java.lang.Object] = null + var methods: Array[java.lang.reflect.Method] = null + + try { + val cls = if (isStatic) { + Utils.classForName(objId) + } else { + objectsTracker.get(objId) match { + case None => throw new IllegalArgumentException("Object not found " + objId) + case Some(o) => + obj = o + o.getClass + } + } + + args = readArgs(numArgs, dis) + methods = cls.getMethods + + val selectedMethods = methods.filter(m => m.getName == methodName) + if (selectedMethods.length > 0) { + val index = findMatchedSignature(selectedMethods.map(_.getParameterTypes), args) + + if (index.isEmpty) { + logWarning( + s"cannot find matching method ${cls}.$methodName. " + + s"Candidates are:") + selectedMethods.foreach { method => + logWarning(s"$methodName(${method.getParameterTypes.mkString(",")})") + } + throw new Exception(s"No matched method found for $cls.$methodName") + } + + val ret = selectedMethods(index.get).invoke(obj, args: _*) + + // Write status bit + serDe.writeInt(dos, 0) + serDe.writeObject(dos, ret.asInstanceOf[AnyRef]) + } else if (methodName == "") { + // methodName should be "" for constructor + val ctor = cls.getConstructors.filter { x => + matchMethod(numArgs, args, x.getParameterTypes) + }.head + + val obj = ctor.newInstance(args: _*) + + serDe.writeInt(dos, 0) + serDe.writeObject(dos, obj.asInstanceOf[AnyRef]) + } else { + throw new IllegalArgumentException( + "invalid method " + methodName + " for object " + objId) + } + } catch { + case e: Throwable => + val jvmObj = objectsTracker.get(objId) + val jvmObjName = jvmObj match { + case Some(jObj) => jObj.getClass.getName + case None => "NullObject" + } + val argsStr = args + .map(arg => { + if (arg != null) { + s"[Type=${arg.getClass.getCanonicalName}, Value: $arg]" + } else { + "[Value: NULL]" + } + }) + .mkString(", ") + + logError(s"Failed to execute '$methodName' on '$jvmObjName' with args=($argsStr)") + + if (methods != null) { + logDebug(s"All methods for $jvmObjName:") + methods.foreach(m => logDebug(m.toString)) + } + + serDe.writeInt(dos, -1) + serDe.writeString(dos, Utils.exceptionString(e.getCause)) + } + } + + // Read a number of arguments from the data input stream + def readArgs(numArgs: Int, dis: DataInputStream): Array[java.lang.Object] = { + (0 until numArgs).map { arg => + serDe.readObject(dis) + }.toArray + } + + // Checks if the arguments passed in args matches the parameter types. + // NOTE: Currently we do exact match. We may add type conversions later. + def matchMethod( + numArgs: Int, + args: Array[java.lang.Object], + parameterTypes: Array[Class[_]]): Boolean = { + if (parameterTypes.length != numArgs) { + return false + } + + for (i <- 0 until numArgs) { + val parameterType = parameterTypes(i) + var parameterWrapperType = parameterType + + // Convert native parameters to Object types as args is Array[Object] here + if (parameterType.isPrimitive) { + parameterWrapperType = parameterType match { + case java.lang.Integer.TYPE => classOf[java.lang.Integer] + case java.lang.Long.TYPE => classOf[java.lang.Long] + case java.lang.Double.TYPE => classOf[java.lang.Double] + case java.lang.Boolean.TYPE => classOf[java.lang.Boolean] + case _ => parameterType + } + } + + if (!parameterWrapperType.isInstance(args(i))) { + // non primitive types + if (!parameterType.isPrimitive && args(i) != null) { + return false + } + + // primitive types + if (parameterType.isPrimitive && !parameterWrapperType.isInstance(args(i))) { + return false + } + } + } + + true + } + + // Find a matching method signature in an array of signatures of constructors + // or methods of the same name according to the passed arguments. Arguments + // may be converted in order to match a signature. + // + // Note that in Java reflection, constructors and normal methods are of different + // classes, and share no parent class that provides methods for reflection uses. + // There is no unified way to handle them in this function. So an array of signatures + // is passed in instead of an array of candidate constructors or methods. + // + // Returns an Option[Int] which is the index of the matched signature in the array. + def findMatchedSignature( + parameterTypesOfMethods: Array[Array[Class[_]]], + args: Array[Object]): Option[Int] = { + val numArgs = args.length + + for (index <- parameterTypesOfMethods.indices) { + val parameterTypes = parameterTypesOfMethods(index) + + if (parameterTypes.length == numArgs) { + var argMatched = true + var i = 0 + while (i < numArgs && argMatched) { + val parameterType = parameterTypes(i) + + if (parameterType == classOf[Seq[Any]] && args(i).getClass.isArray) { + // The case that the parameter type is a Scala Seq and the argument + // is a Java array is considered matching. The array will be converted + // to a Seq later if this method is matched. + } else { + var parameterWrapperType = parameterType + + // Convert native parameters to Object types as args is Array[Object] here + if (parameterType.isPrimitive) { + parameterWrapperType = parameterType match { + case java.lang.Integer.TYPE => classOf[java.lang.Integer] + case java.lang.Long.TYPE => classOf[java.lang.Long] + case java.lang.Double.TYPE => classOf[java.lang.Double] + case java.lang.Boolean.TYPE => classOf[java.lang.Boolean] + case _ => parameterType + } + } + if ((parameterType.isPrimitive || args(i) != null) && + !parameterWrapperType.isInstance(args(i))) { + argMatched = false + } + } + + i = i + 1 + } + + if (argMatched) { + // For now, we return the first matching method. + // TODO: find best method in matching methods. + + // Convert args if needed + val parameterTypes = parameterTypesOfMethods(index) + + for (i <- 0 until numArgs) { + if (parameterTypes(i) == classOf[Seq[Any]] && args(i).getClass.isArray) { + // Convert a Java array to scala Seq + args(i) = args(i).asInstanceOf[Array[_]].toSeq + } + } + + return Some(index) + } + } + } + None + } + + def logError(id: String, e: Exception): Unit = {} +} + + diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetException.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetException.scala new file mode 100644 index 00000000..c70d16b0 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetException.scala @@ -0,0 +1,13 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.api.dotnet + +class DotnetException(message: String, cause: Throwable) + extends Exception(message, cause) { + + def this(message: String) = this(message, null) +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetRDD.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetRDD.scala new file mode 100644 index 00000000..f5277c21 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetRDD.scala @@ -0,0 +1,30 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.api.dotnet + +import org.apache.spark.SparkContext +import org.apache.spark.api.java.JavaRDD +import org.apache.spark.api.python._ +import org.apache.spark.rdd.RDD + +object DotnetRDD { + def createPythonRDD( + parent: RDD[_], + func: PythonFunction, + preservePartitoning: Boolean): PythonRDD = { + new PythonRDD(parent, func, preservePartitoning) + } + + def createJavaRDDFromArray( + sc: SparkContext, + arr: Array[Array[Byte]], + numSlices: Int): JavaRDD[Array[Byte]] = { + JavaRDD.fromRDD(sc.parallelize(arr, numSlices)) + } + + def toJavaRDD(rdd: RDD[_]): JavaRDD[_] = JavaRDD.fromRDD(rdd) +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetUtils.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetUtils.scala new file mode 100644 index 00000000..9f556338 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/DotnetUtils.scala @@ -0,0 +1,39 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.api.dotnet + +import scala.collection.JavaConverters._ + +/** DotnetUtils object that hosts some helper functions + * help data type conversions between dotnet and scala + */ +object DotnetUtils { + + /** A helper function to convert scala Map to java.util.Map + * @param value - scala Map + * @return java.util.Map + */ + def convertToJavaMap(value: Map[_, _]): java.util.Map[_, _] = value.asJava + + /** Convert java data type to corresponding scala type + * @param value - java.lang.Object + * @return Any + */ + def mapScalaToJava(value: java.lang.Object): Any = { + value match { + case i: java.lang.Integer => i.toInt + case d: java.lang.Double => d.toDouble + case f: java.lang.Float => f.toFloat + case b: java.lang.Boolean => b.booleanValue() + case l: java.lang.Long => l.toLong + case s: java.lang.Short => s.toShort + case by: java.lang.Byte => by.toByte + case c: java.lang.Character => c.toChar + case _ => value + } + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/JVMObjectTracker.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/JVMObjectTracker.scala new file mode 100644 index 00000000..81cfaf88 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/JVMObjectTracker.scala @@ -0,0 +1,55 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + + +package org.apache.spark.api.dotnet + +import scala.collection.mutable.HashMap + +/** + * Tracks JVM objects returned to .NET which is useful for invoking calls from .NET on JVM objects. + */ +private[dotnet] class JVMObjectTracker { + + // Multiple threads may access objMap and increase objCounter. Because get method return Option, + // it is convenient to use a Scala map instead of java.util.concurrent.ConcurrentHashMap. + private[this] val objMap = new HashMap[String, Object] + private[this] var objCounter: Int = 1 + + def getObject(id: String): Object = { + synchronized { + objMap(id) + } + } + + def get(id: String): Option[Object] = { + synchronized { + objMap.get(id) + } + } + + def put(obj: Object): String = { + synchronized { + val objId = objCounter.toString + objCounter = objCounter + 1 + objMap.put(objId, obj) + objId + } + } + + def remove(id: String): Option[Object] = { + synchronized { + objMap.remove(id) + } + } + + def clear(): Unit = { + synchronized { + objMap.clear() + objCounter = 1 + } + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/JvmBridgeUtils.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/JvmBridgeUtils.scala new file mode 100644 index 00000000..06a476f6 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/JvmBridgeUtils.scala @@ -0,0 +1,33 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.sql.api.dotnet + +import org.apache.spark.SparkConf + +/* + * Utils for JvmBridge. + */ +object JvmBridgeUtils { + def getKeyValuePairAsString(kvp: (String, String)): String = { + return kvp._1 + "=" + kvp._2 + } + + def getKeyValuePairArrayAsString(kvpArray: Array[(String, String)]): String = { + val sb = new StringBuilder + + for (kvp <- kvpArray) { + sb.append(getKeyValuePairAsString(kvp)) + sb.append(";") + } + + sb.toString + } + + def getSparkConfAsString(sparkConf: SparkConf): String = { + getKeyValuePairArrayAsString(sparkConf.getAll) + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/SerDe.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/SerDe.scala new file mode 100644 index 00000000..bd380b7d --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/SerDe.scala @@ -0,0 +1,387 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.api.dotnet + +import java.io.{DataInputStream, DataOutputStream} +import java.nio.charset.StandardCharsets +import java.sql.{Date, Time, Timestamp} + +import org.apache.spark.sql.Row + +import scala.collection.JavaConverters._ + +/** + * Class responsible for serialization and deserialization between CLR & JVM. + * This implementation of methods is mostly identical to the SerDe implementation in R. + */ +class SerDe(val tracker: JVMObjectTracker) { + + def readObjectType(dis: DataInputStream): Char = { + dis.readByte().toChar + } + + def readObject(dis: DataInputStream): Object = { + val dataType = readObjectType(dis) + readTypedObject(dis, dataType) + } + + private def readTypedObject(dis: DataInputStream, dataType: Char): Object = { + dataType match { + case 'n' => null + case 'i' => new java.lang.Integer(readInt(dis)) + case 'g' => new java.lang.Long(readLong(dis)) + case 'd' => new java.lang.Double(readDouble(dis)) + case 'b' => new java.lang.Boolean(readBoolean(dis)) + case 'c' => readString(dis) + case 'e' => readMap(dis) + case 'r' => readBytes(dis) + case 'l' => readList(dis) + case 'D' => readDate(dis) + case 't' => readTime(dis) + case 'j' => tracker.getObject(readString(dis)) + case 'R' => readRowArr(dis) + case 'O' => readObjectArr(dis) + case _ => throw new IllegalArgumentException(s"Invalid type $dataType") + } + } + + private def readBytes(in: DataInputStream): Array[Byte] = { + val len = readInt(in) + val out = new Array[Byte](len) + in.readFully(out) + out + } + + def readInt(in: DataInputStream): Int = { + in.readInt() + } + + private def readLong(in: DataInputStream): Long = { + in.readLong() + } + + private def readDouble(in: DataInputStream): Double = { + in.readDouble() + } + + private def readStringBytes(in: DataInputStream, len: Int): String = { + val bytes = new Array[Byte](len) + in.readFully(bytes) + val str = new String(bytes, "UTF-8") + str + } + + def readString(in: DataInputStream): String = { + val len = in.readInt() + readStringBytes(in, len) + } + + def readBoolean(in: DataInputStream): Boolean = { + in.readBoolean() + } + + private def readDate(in: DataInputStream): Date = { + Date.valueOf(readString(in)) + } + + private def readTime(in: DataInputStream): Timestamp = { + val seconds = in.readDouble() + val sec = Math.floor(seconds).toLong + val t = new Timestamp(sec * 1000L) + t.setNanos(((seconds - sec) * 1e9).toInt) + t + } + + private def readRow(in: DataInputStream): Row = { + val len = readInt(in) + Row.fromSeq((0 until len).map(_ => readObject(in))) + } + + private def readBytesArr(in: DataInputStream): Array[Array[Byte]] = { + val len = readInt(in) + (0 until len).map(_ => readBytes(in)).toArray + } + + private def readIntArr(in: DataInputStream): Array[Int] = { + val len = readInt(in) + (0 until len).map(_ => readInt(in)).toArray + } + + private def readLongArr(in: DataInputStream): Array[Long] = { + val len = readInt(in) + (0 until len).map(_ => readLong(in)).toArray + } + + private def readDoubleArr(in: DataInputStream): Array[Double] = { + val len = readInt(in) + (0 until len).map(_ => readDouble(in)).toArray + } + + private def readDoubleArrArr(in: DataInputStream): Array[Array[Double]] = { + val len = readInt(in) + (0 until len).map(_ => readDoubleArr(in)).toArray + } + + private def readBooleanArr(in: DataInputStream): Array[Boolean] = { + val len = readInt(in) + (0 until len).map(_ => readBoolean(in)).toArray + } + + private def readStringArr(in: DataInputStream): Array[String] = { + val len = readInt(in) + (0 until len).map(_ => readString(in)).toArray + } + + private def readRowArr(in: DataInputStream): java.util.List[Row] = { + val len = readInt(in) + (0 until len).map(_ => readRow(in)).toList.asJava + } + + private def readObjectArr(in: DataInputStream): Seq[Any] = { + val len = readInt(in) + (0 until len).map(_ => readObject(in)) + } + + private def readList(dis: DataInputStream): Array[_] = { + val arrType = readObjectType(dis) + arrType match { + case 'i' => readIntArr(dis) + case 'g' => readLongArr(dis) + case 'c' => readStringArr(dis) + case 'd' => readDoubleArr(dis) + case 'A' => readDoubleArrArr(dis) + case 'b' => readBooleanArr(dis) + case 'j' => readStringArr(dis).map(x => tracker.getObject(x)) + case 'r' => readBytesArr(dis) + case _ => throw new IllegalArgumentException(s"Invalid array type $arrType") + } + } + + private def readMap(in: DataInputStream): java.util.Map[Object, Object] = { + val len = readInt(in) + if (len > 0) { + val keysType = readObjectType(in) + val keysLen = readInt(in) + val keys = (0 until keysLen).map(_ => readTypedObject(in, keysType)) + + val valuesLen = readInt(in) + val values = (0 until valuesLen).map(_ => { + val valueType = readObjectType(in) + readTypedObject(in, valueType) + }) + keys.zip(values).toMap.asJava + } else { + new java.util.HashMap[Object, Object]() + } + } + + // Using the same mapping as SparkR implementation for now + // Methods to write out data from Java to .NET. + // + // Type mapping from Java to .NET: + // + // void -> NULL + // Int -> integer + // String -> character + // Boolean -> logical + // Float -> double + // Double -> double + // Long -> long + // Array[Byte] -> raw + // Date -> Date + // Time -> POSIXct + // + // Array[T] -> list() + // Object -> jobj + + def writeType(dos: DataOutputStream, typeStr: String): Unit = { + typeStr match { + case "void" => dos.writeByte('n') + case "character" => dos.writeByte('c') + case "double" => dos.writeByte('d') + case "doublearray" => dos.writeByte('A') + case "long" => dos.writeByte('g') + case "integer" => dos.writeByte('i') + case "logical" => dos.writeByte('b') + case "date" => dos.writeByte('D') + case "time" => dos.writeByte('t') + case "raw" => dos.writeByte('r') + case "list" => dos.writeByte('l') + case "jobj" => dos.writeByte('j') + case _ => throw new IllegalArgumentException(s"Invalid type $typeStr") + } + } + + def writeObject(dos: DataOutputStream, value: Object): Unit = { + if (value == null || value.isInstanceOf[Unit]) { + writeType(dos, "void") + } else { + value.getClass.getName match { + case "java.lang.String" => + writeType(dos, "character") + writeString(dos, value.asInstanceOf[String]) + case "float" | "java.lang.Float" => + writeType(dos, "double") + writeDouble(dos, value.asInstanceOf[Float].toDouble) + case "double" | "java.lang.Double" => + writeType(dos, "double") + writeDouble(dos, value.asInstanceOf[Double]) + case "long" | "java.lang.Long" => + writeType(dos, "long") + writeLong(dos, value.asInstanceOf[Long]) + case "int" | "java.lang.Integer" => + writeType(dos, "integer") + writeInt(dos, value.asInstanceOf[Int]) + case "boolean" | "java.lang.Boolean" => + writeType(dos, "logical") + writeBoolean(dos, value.asInstanceOf[Boolean]) + case "java.sql.Date" => + writeType(dos, "date") + writeDate(dos, value.asInstanceOf[Date]) + case "java.sql.Time" => + writeType(dos, "time") + writeTime(dos, value.asInstanceOf[Time]) + case "java.sql.Timestamp" => + writeType(dos, "time") + writeTime(dos, value.asInstanceOf[Timestamp]) + case "[B" => + writeType(dos, "raw") + writeBytes(dos, value.asInstanceOf[Array[Byte]]) + // TODO: Types not handled right now include + // byte, char, short, float + + // Handle arrays + case "[Ljava.lang.String;" => + writeType(dos, "list") + writeStringArr(dos, value.asInstanceOf[Array[String]]) + case "[I" => + writeType(dos, "list") + writeIntArr(dos, value.asInstanceOf[Array[Int]]) + case "[J" => + writeType(dos, "list") + writeLongArr(dos, value.asInstanceOf[Array[Long]]) + case "[D" => + writeType(dos, "list") + writeDoubleArr(dos, value.asInstanceOf[Array[Double]]) + case "[[D" => + writeType(dos, "list") + writeDoubleArrArr(dos, value.asInstanceOf[Array[Array[Double]]]) + case "[Z" => + writeType(dos, "list") + writeBooleanArr(dos, value.asInstanceOf[Array[Boolean]]) + case "[[B" => + writeType(dos, "list") + writeBytesArr(dos, value.asInstanceOf[Array[Array[Byte]]]) + case otherName => + // Handle array of objects + if (otherName.startsWith("[L")) { + val objArr = value.asInstanceOf[Array[Object]] + writeType(dos, "list") + writeType(dos, "jobj") + dos.writeInt(objArr.length) + objArr.foreach(o => writeJObj(dos, o)) + } else { + writeType(dos, "jobj") + writeJObj(dos, value) + } + } + } + } + + def writeInt(out: DataOutputStream, value: Int): Unit = { + out.writeInt(value) + } + + def writeLong(out: DataOutputStream, value: Long): Unit = { + out.writeLong(value) + } + + private def writeDouble(out: DataOutputStream, value: Double): Unit = { + out.writeDouble(value) + } + + private def writeBoolean(out: DataOutputStream, value: Boolean): Unit = { + out.writeBoolean(value) + } + + private def writeDate(out: DataOutputStream, value: Date): Unit = { + writeString(out, value.toString) + } + + private def writeTime(out: DataOutputStream, value: Time): Unit = { + out.writeDouble(value.getTime.toDouble / 1000.0) + } + + private def writeTime(out: DataOutputStream, value: Timestamp): Unit = { + out.writeDouble((value.getTime / 1000).toDouble + value.getNanos.toDouble / 1e9) + } + + def writeString(out: DataOutputStream, value: String): Unit = { + val utf8 = value.getBytes(StandardCharsets.UTF_8) + val len = utf8.length + out.writeInt(len) + out.write(utf8, 0, len) + } + + private def writeBytes(out: DataOutputStream, value: Array[Byte]): Unit = { + out.writeInt(value.length) + out.write(value) + } + + def writeJObj(out: DataOutputStream, value: Object): Unit = { + val objId = tracker.put(value) + writeString(out, objId) + } + + private def writeIntArr(out: DataOutputStream, value: Array[Int]): Unit = { + writeType(out, "integer") + out.writeInt(value.length) + value.foreach(v => out.writeInt(v)) + } + + private def writeLongArr(out: DataOutputStream, value: Array[Long]): Unit = { + writeType(out, "long") + out.writeInt(value.length) + value.foreach(v => out.writeLong(v)) + } + + private def writeDoubleArr(out: DataOutputStream, value: Array[Double]): Unit = { + writeType(out, "double") + out.writeInt(value.length) + value.foreach(v => out.writeDouble(v)) + } + + private def writeDoubleArrArr(out: DataOutputStream, value: Array[Array[Double]]): Unit = { + writeType(out, "doublearray") + out.writeInt(value.length) + value.foreach(v => writeDoubleArr(out, v)) + } + + private def writeBooleanArr(out: DataOutputStream, value: Array[Boolean]): Unit = { + writeType(out, "logical") + out.writeInt(value.length) + value.foreach(v => writeBoolean(out, v)) + } + + private def writeStringArr(out: DataOutputStream, value: Array[String]): Unit = { + writeType(out, "character") + out.writeInt(value.length) + value.foreach(v => writeString(out, v)) + } + + private def writeBytesArr(out: DataOutputStream, value: Array[Array[Byte]]): Unit = { + writeType(out, "raw") + out.writeInt(value.length) + value.foreach(v => writeBytes(out, v)) + } +} + +private object SerializationFormats { + val BYTE = "byte" + val STRING = "string" + val ROW = "row" +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/ThreadPool.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/ThreadPool.scala new file mode 100644 index 00000000..50551a7d --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/api/dotnet/ThreadPool.scala @@ -0,0 +1,72 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.api.dotnet + +import java.util.concurrent.{ExecutorService, Executors} + +import scala.collection.mutable + +/** + * Pool of thread executors. There should be a 1-1 correspondence between C# threads + * and Java threads. + */ +object ThreadPool { + + /** + * Map from (processId, threadId) to corresponding executor. + */ + private val executors: mutable.HashMap[(Int, Int), ExecutorService] = + new mutable.HashMap[(Int, Int), ExecutorService]() + + /** + * Run some code on a particular thread. + * @param processId Integer id of the process. + * @param threadId Integer id of the thread. + * @param task Function to run on the thread. + */ + def run(processId: Int, threadId: Int, task: () => Unit): Unit = { + val executor = getOrCreateExecutor(processId, threadId) + val future = executor.submit(new Runnable { + override def run(): Unit = task() + }) + + future.get() + } + + /** + * Try to delete a particular thread. + * @param processId Integer id of the process. + * @param threadId Integer id of the thread. + * @return True if successful, false if thread does not exist. + */ + def tryDeleteThread(processId: Int, threadId: Int): Boolean = synchronized { + executors.remove((processId, threadId)) match { + case Some(executorService) => + executorService.shutdown() + true + case None => false + } + } + + /** + * Shutdown any running ExecutorServices. + */ + def shutdown(): Unit = synchronized { + executors.foreach(_._2.shutdown()) + executors.clear() + } + + /** + * Get the executor if it exists, otherwise create a new one. + * @param processId Integer id of the process. + * @param threadId Integer id of the thread. + * @return The new or existing executor with the given id. + */ + private def getOrCreateExecutor(processId: Int, threadId: Int): ExecutorService = synchronized { + executors.getOrElseUpdate((processId, threadId), Executors.newSingleThreadExecutor) + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/deploy/dotnet/DotNetUserAppException.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/deploy/dotnet/DotNetUserAppException.scala new file mode 100644 index 00000000..4551a70b --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/deploy/dotnet/DotNetUserAppException.scala @@ -0,0 +1,22 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.deploy.dotnet + +import org.apache.spark.SparkException + +/** + * This exception type describes an exception thrown by a .NET user application. + * + * @param exitCode Exit code returned by the .NET application. + * @param dotNetStackTrace Stacktrace extracted from .NET application logs. + */ +private[spark] class DotNetUserAppException(exitCode: Int, dotNetStackTrace: Option[String]) + extends SparkException( + dotNetStackTrace match { + case None => s"User application exited with $exitCode" + case Some(e) => s"User application exited with $exitCode and .NET exception: $e" + }) diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/deploy/dotnet/DotnetRunner.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/deploy/dotnet/DotnetRunner.scala new file mode 100644 index 00000000..56cbfe8b --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/deploy/dotnet/DotnetRunner.scala @@ -0,0 +1,309 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.deploy.dotnet + +import java.io.File +import java.net.URI +import java.nio.file.attribute.PosixFilePermissions +import java.nio.file.{FileSystems, Files, Paths} +import java.util.Locale +import java.util.concurrent.{Semaphore, TimeUnit} + +import org.apache.commons.io.FilenameUtils +import org.apache.commons.io.output.TeeOutputStream +import org.apache.hadoop.fs.Path +import org.apache.spark +import org.apache.spark.api.dotnet.DotnetBackend +import org.apache.spark.deploy.{PythonRunner, SparkHadoopUtil} +import org.apache.spark.internal.Logging +import org.apache.spark.internal.config.dotnet.Dotnet.{ + DOTNET_IGNORE_SPARK_PATCH_VERSION_CHECK, + ERROR_BUFFER_SIZE, ERROR_REDIRECITON_ENABLED +} +import org.apache.spark.util.dotnet.{Utils => DotnetUtils} +import org.apache.spark.util.{CircularBuffer, RedirectThread, Utils} +import org.apache.spark.{SparkConf, SparkUserAppException} + +import scala.jdk.CollectionConverters._ +import scala.io.StdIn +import scala.util.Try + +/** + * DotnetRunner class used to launch Spark .NET applications using spark-submit. + * It executes .NET application as a subprocess and then has it connect back to + * the JVM to access system properties etc. + */ +object DotnetRunner extends Logging { + private val DEBUG_PORT = 5567 + private val supportedSparkMajorMinorVersionPrefix = "4.1" + private val supportedSparkVersions = Set[String]("4.1.0", "4.1.1") + + val SPARK_VERSION = DotnetUtils.normalizeSparkVersion(spark.SPARK_VERSION) + + def main(args: Array[String]): Unit = { + if (args.length == 0) { + throw new IllegalArgumentException("At least one argument is expected.") + } + + DotnetUtils.validateSparkVersions( + sys.props + .getOrElse( + DOTNET_IGNORE_SPARK_PATCH_VERSION_CHECK.key, + DOTNET_IGNORE_SPARK_PATCH_VERSION_CHECK.defaultValue.get.toString) + .toBoolean, + spark.SPARK_VERSION, + SPARK_VERSION, + supportedSparkMajorMinorVersionPrefix, + supportedSparkVersions) + + val settings = initializeSettings(args) + + // Determines if this needs to be run in debug mode. + // In debug mode this runner will not launch a .NET process. + val runInDebugMode = settings._1 + @volatile var dotnetBackendPortNumber = settings._2 + var dotnetExecutable = "" + var otherArgs: Array[String] = null + + if (!runInDebugMode) { + if (args(0).toLowerCase(Locale.ROOT).endsWith(".zip")) { + var zipFileName = args(0) + val zipFileUri = Try(new URI(zipFileName)).getOrElse(new File(zipFileName).toURI) + val workingDir = new File("").getAbsoluteFile + val driverDir = new File(workingDir, FilenameUtils.getBaseName(zipFileUri.getPath())) + + // Standalone cluster mode where .NET application is remotely located. + if (zipFileUri.getScheme() != "file") { + zipFileName = downloadDriverFile(zipFileName, workingDir.getAbsolutePath).getName + } + + logInfo(s"Unzipping .NET driver $zipFileName to $driverDir") + DotnetUtils.unzip(new File(zipFileName), driverDir) + + // Reuse windows-specific formatting in PythonRunner. + dotnetExecutable = PythonRunner.formatPath(resolveDotnetExecutable(driverDir, args(1))) + otherArgs = args.slice(2, args.length) + } else { + // Reuse windows-specific formatting in PythonRunner. + dotnetExecutable = PythonRunner.formatPath(args(0)) + otherArgs = args.slice(1, args.length) + } + } else { + otherArgs = args.slice(1, args.length) + } + + val processParameters = new java.util.ArrayList[String] + processParameters.add(dotnetExecutable) + otherArgs.foreach(arg => processParameters.add(arg)) + + logInfo(s"Starting DotnetBackend with $dotnetExecutable.") + + // Time to wait for DotnetBackend to initialize in seconds. + val backendTimeout = sys.env.getOrElse("DOTNETBACKEND_TIMEOUT", "120").toInt + + // Launch a DotnetBackend server for the .NET process to connect to; this will let it see our + // Java system properties etc. + val dotnetBackend = new DotnetBackend() + val initialized = new Semaphore(0) + val dotnetBackendThread = new Thread("DotnetBackend") { + override def run() { + // need to get back dotnetBackendPortNumber because if the value passed to init is 0 + // the port number is dynamically assigned in the backend + dotnetBackendPortNumber = dotnetBackend.init(dotnetBackendPortNumber) + logInfo(s"Port number used by DotnetBackend is $dotnetBackendPortNumber") + initialized.release() + dotnetBackend.run() + } + } + + dotnetBackendThread.start() + + if (initialized.tryAcquire(backendTimeout, TimeUnit.SECONDS)) { + if (!runInDebugMode) { + var returnCode = -1 + var process: Process = null + val enableLogRedirection: Boolean = sys.props + .getOrElse( + ERROR_REDIRECITON_ENABLED.key, + ERROR_REDIRECITON_ENABLED.defaultValue.get.toString).toBoolean + val stderrBuffer: Option[CircularBuffer] = Option(enableLogRedirection).collect { + case true => new CircularBuffer( + sys.props.getOrElse( + ERROR_BUFFER_SIZE.key, + ERROR_BUFFER_SIZE.defaultValue.get.toString).toInt) + } + + try { + val builder = new ProcessBuilder(processParameters) + val env = builder.environment() + env.put("DOTNETBACKEND_PORT", dotnetBackendPortNumber.toString) + + for ((key, value) <- Utils.getSystemProperties if key.startsWith("spark.")) { + env.put(key, value) + logInfo(s"Adding key=$key and value=$value to environment") + } + builder.redirectErrorStream(true) // Ugly but needed for stdout and stderr to synchronize + process = builder.start() + + // Redirect stdin of JVM process to stdin of .NET process. + new RedirectThread(System.in, process.getOutputStream, "redirect JVM input").start() + // Redirect stdout and stderr of .NET process to System.out and to buffer + // if log direction is enabled. If not, redirect only to System.out. + new RedirectThread( + process.getInputStream, + stderrBuffer match { + case Some(buffer) => new TeeOutputStream(System.out, buffer) + case _ => System.out + }, + "redirect .NET stdout and stderr").start() + + process.waitFor() + } catch { + case t: Throwable => + logThrowable(t) + } finally { + returnCode = closeDotnetProcess(process) + closeBackend(dotnetBackend) + } + if (returnCode != 0) { + if (stderrBuffer.isDefined) { + throw new DotNetUserAppException(returnCode, Some(stderrBuffer.get.toString)) + } else { + throw new SparkUserAppException(returnCode) + } + } else { + logInfo(s".NET application exited successfully") + } + // TODO: The following is causing the following error: + // INFO ApplicationMaster: Final app status: FAILED, exitCode: 16, + // (reason: Shutdown hook called before final status was reported.) + // DotnetUtils.exit(returnCode) + } else { + // scalastyle:off println + println("***********************************************************************") + println("* .NET Backend running debug mode. Press enter to exit *") + println("***********************************************************************") + // scalastyle:on println + + StdIn.readLine() + closeBackend(dotnetBackend) + DotnetUtils.exit(0) + } + } else { + logError(s"DotnetBackend did not initialize in $backendTimeout seconds") + DotnetUtils.exit(-1) + } + } + + // When the executable is downloaded as part of zip file, check if the file exists + // after zip file is unzipped under the given dir. Once it is found, change the + // permission to executable (only for Unix systems, since the zip file may have been + // created under Windows. Finally, the absolute path for the executable is returned. + private def resolveDotnetExecutable(dir: File, dotnetExecutable: String): String = { + val path = Paths.get(dir.getAbsolutePath, dotnetExecutable) + val resolvedExecutable = if (Files.isRegularFile(path)) { + path.toAbsolutePath.toString + } else { + Files + .walk(FileSystems.getDefault.getPath(dir.getAbsolutePath)) + .iterator() + .asScala + .find(path => Files.isRegularFile(path) && path.getFileName.toString == dotnetExecutable) match { + case Some(path) => path.toAbsolutePath.toString + case None => + throw new IllegalArgumentException( + s"Failed to find $dotnetExecutable under ${dir.getAbsolutePath}") + } + } + + if (DotnetUtils.supportPosix) { + Files.setPosixFilePermissions( + Paths.get(resolvedExecutable), + PosixFilePermissions.fromString("rwxr-xr-x")) + } + + resolvedExecutable + } + + /** + * Download HDFS file into the supplied directory and return its local path. + * Will throw an exception if there are errors during downloading. + */ + private def downloadDriverFile(hdfsFilePath: String, driverDir: String): File = { + val sparkConf = new SparkConf() + val filePath = new Path(hdfsFilePath) + + val hadoopConf = SparkHadoopUtil.get.newConfiguration(sparkConf) + val jarFileName = filePath.getName + val localFile = new File(driverDir, jarFileName) + + if (!localFile.exists()) { // May already exist if running multiple workers on one node + logInfo(s"Copying user file $filePath to $driverDir") + DotnetUtils.fetchFileWithbackwardCompatibility( + hdfsFilePath, + new File(driverDir), + sparkConf, + hadoopConf, + System.currentTimeMillis(), + useCache = false) + } + + if (!localFile.exists()) { + throw new Exception(s"Did not see expected $jarFileName in $driverDir") + } + + localFile + } + + private def closeBackend(dotnetBackend: DotnetBackend): Unit = { + logInfo("Closing DotnetBackend") + dotnetBackend.close() + } + + private def closeDotnetProcess(dotnetProcess: Process): Int = { + if (dotnetProcess == null) { + return -1 + } else if (!dotnetProcess.isAlive) { + return dotnetProcess.exitValue() + } + + // Try to (gracefully on Linux) kill the process and resort to force if interrupted + var returnCode = -1 + logInfo("Closing .NET process") + try { + dotnetProcess.destroy() + returnCode = dotnetProcess.waitFor() + } catch { + case _: InterruptedException => + logInfo( + "Thread interrupted while waiting for graceful close. Forcefully closing .NET process") + returnCode = dotnetProcess.destroyForcibly().waitFor() + case t: Throwable => + logThrowable(t) + } + + returnCode + } + + private def initializeSettings(args: Array[String]): (Boolean, Int) = { + val runInDebugMode = (args.length == 1 || args.length == 2) && args(0).equalsIgnoreCase( + "debug") + var portNumber = 0 + if (runInDebugMode) { + if (args.length == 1) { + portNumber = DEBUG_PORT + } else if (args.length == 2) { + portNumber = Integer.parseInt(args(1)) + } + } + + (runInDebugMode, portNumber) + } + + private def logThrowable(throwable: Throwable): Unit = + logError(s"${throwable.getMessage} \n ${throwable.getStackTrace.mkString("\n")}") +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/internal/config/dotnet/Dotnet.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/internal/config/dotnet/Dotnet.scala new file mode 100644 index 00000000..18ba4c6e --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/internal/config/dotnet/Dotnet.scala @@ -0,0 +1,28 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.internal.config.dotnet + +import org.apache.spark.internal.config.ConfigBuilder + +private[spark] object Dotnet { + val DOTNET_NUM_BACKEND_THREADS = ConfigBuilder("spark.dotnet.numDotnetBackendThreads").intConf + .createWithDefault(10) + + val DOTNET_IGNORE_SPARK_PATCH_VERSION_CHECK = + ConfigBuilder("spark.dotnet.ignoreSparkPatchVersionCheck").booleanConf + .createWithDefault(false) + + val ERROR_REDIRECITON_ENABLED = + ConfigBuilder("spark.nonjvm.error.forwarding.enabled").booleanConf + .createWithDefault(false) + + val ERROR_BUFFER_SIZE = + ConfigBuilder("spark.nonjvm.error.buffer.size") + .intConf + .checkValue(_ >= 0, "The error buffer size must not be negative") + .createWithDefault(10240) +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/mllib/api/dotnet/MLUtils.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/mllib/api/dotnet/MLUtils.scala new file mode 100644 index 00000000..3e3c3e0e --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/mllib/api/dotnet/MLUtils.scala @@ -0,0 +1,26 @@ + +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.mllib.api.dotnet + +import org.apache.spark.ml._ +import scala.collection.JavaConverters._ + +/** MLUtils object that hosts helper functions + * related to ML usage + */ +object MLUtils { + + /** A helper function to let pipeline accept java.util.ArrayList + * format stages in scala code + * @param pipeline - The pipeline to be set stages + * @param value - A java.util.ArrayList of PipelineStages to be set as stages + * @return The pipeline + */ + def setPipelineStages(pipeline: Pipeline, value: java.util.ArrayList[_ <: PipelineStage]): Pipeline = + pipeline.setStages(value.asScala.toArray) +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/sql/api/dotnet/DotnetForeachBatch.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/sql/api/dotnet/DotnetForeachBatch.scala new file mode 100644 index 00000000..5d06d430 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/sql/api/dotnet/DotnetForeachBatch.scala @@ -0,0 +1,33 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.sql.api.dotnet + +import org.apache.spark.api.dotnet.CallbackClient +import org.apache.spark.internal.Logging +import org.apache.spark.sql.{DataFrame, Row} +import org.apache.spark.sql.streaming.DataStreamWriter + +class DotnetForeachBatchFunction(callbackClient: CallbackClient, callbackId: Int) extends Logging { + def call(batchDF: DataFrame, batchId: Long): Unit = + callbackClient.send( + callbackId, + (dos, serDe) => { + serDe.writeJObj(dos, batchDF) + serDe.writeLong(dos, batchId) + }) +} + +object DotnetForeachBatchHelper { + def callForeachBatch(client: Option[CallbackClient], dsw: DataStreamWriter[Row], callbackId: Int): Unit = { + val dotnetForeachFunc = client match { + case Some(value) => new DotnetForeachBatchFunction(value, callbackId) + case None => throw new Exception("CallbackClient is null.") + } + + dsw.foreachBatch(dotnetForeachFunc.call _) + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/sql/api/dotnet/SQLUtils.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/sql/api/dotnet/SQLUtils.scala new file mode 100644 index 00000000..31dcd061 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/sql/api/dotnet/SQLUtils.scala @@ -0,0 +1,37 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.sql.api.dotnet + +import java.util.{List => JList, Map => JMap} + +import org.apache.spark.api.python.{PythonAccumulatorV2, PythonBroadcast, PythonFunction, SimplePythonFunction} +import org.apache.spark.broadcast.Broadcast + +object SQLUtils { + + /** + * Exposes createPythonFunction to the .NET client to enable registering UDFs. + */ + def createPythonFunction( + command: Array[Byte], + envVars: JMap[String, String], + pythonIncludes: JList[String], + pythonExec: String, + pythonVersion: String, + broadcastVars: JList[Broadcast[PythonBroadcast]], + accumulator: PythonAccumulatorV2): PythonFunction = { + // From 3.4.0 use SimplePythonFunction. https://github.com/apache/spark/commit/18ff15729268def5ee1bdf5dfcb766bd1d699684 + SimplePythonFunction( + command, + envVars, + pythonIncludes, + pythonExec, + pythonVersion, + broadcastVars, + accumulator) + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/util/dotnet/Utils.scala b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/util/dotnet/Utils.scala new file mode 100644 index 00000000..4db64372 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/main/scala/org/apache/spark/util/dotnet/Utils.scala @@ -0,0 +1,305 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.util.dotnet + +import java.io._ +import java.nio.file.attribute.PosixFilePermission +import java.nio.file.attribute.PosixFilePermission._ +import java.nio.file.{FileSystems, Files} +import java.util.{Timer, TimerTask} +import org.apache.spark.SparkConf +import org.apache.hadoop.conf.Configuration +import org.apache.spark.util.Utils +import java.io.File +import java.lang.NoSuchMethodException +import java.lang.reflect.InvocationTargetException +import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream, ZipFile} +import org.apache.commons.io.{FileUtils, IOUtils} +import org.apache.spark.internal.Logging +import org.apache.spark.internal.config.dotnet.Dotnet.DOTNET_IGNORE_SPARK_PATCH_VERSION_CHECK + +import scala.jdk.CollectionConverters._ +import scala.collection.Set + +/** + * Utility methods. + */ +object Utils extends Logging { + private val posixFilePermissions = Array( + OWNER_READ, + OWNER_WRITE, + OWNER_EXECUTE, + GROUP_READ, + GROUP_WRITE, + GROUP_EXECUTE, + OTHERS_READ, + OTHERS_WRITE, + OTHERS_EXECUTE) + + val supportPosix: Boolean = + FileSystems.getDefault.supportedFileAttributeViews().contains("posix") + + /** + * Provides a backward-compatible implementation of the `fetchFile` method + * from Apache Spark's `org.apache.spark.util.Utils` class. + * + * This method handles differences in method signatures between Spark versions, + * specifically the inclusion or absence of a `SecurityManager` parameter. It uses + * reflection to dynamically resolve and invoke the correct version of `fetchFile`. + * + * @param url The source URL of the file to be fetched. + * @param targetDir The directory where the fetched file will be saved. + * @param conf The Spark configuration object used to determine runtime settings. + * @param hadoopConf Hadoop configuration settings for file access. + * @param timestamp A timestamp indicating the cache validity of the fetched file. + * @param useCache Whether to use Spark's caching mechanism to reuse previously downloaded files. + * @param shouldUntar Whether to untar the downloaded file if it is a tarball. Defaults to `true`. + * + * @return A `File` object pointing to the fetched and stored file. + * + * @throws IllegalArgumentException If neither method signature is found. + * @throws Throwable If an error occurs during reflection or method invocation. + * + * Note: + * - This method was introduced as a fix for DataBricks-specific file copying issues + * and was referenced in PR #1048. + * - Reflection is used to ensure compatibility across Spark environments. + */ + def fetchFileWithbackwardCompatibility( + url: String, + targetDir: File, + conf: SparkConf, + hadoopConf: Configuration, + timestamp: Long, + useCache: Boolean, + shouldUntar: Boolean = true): File = { + + // Spark 4.x removed SecurityManager from Utils.fetchFile — call directly without reflection. + org.apache.spark.util.Utils.fetchFile( + url, + targetDir, + conf, + hadoopConf, + timestamp, + useCache, + shouldUntar) + } + + /** + * Compress all files under given directory into one zip file and drop it to the target directory + * + * @param sourceDir source directory to zip + * @param targetZipFile target zip file + */ + def zip(sourceDir: File, targetZipFile: File): Unit = { + var fos: FileOutputStream = null + var zos: ZipArchiveOutputStream = null + try { + fos = new FileOutputStream(targetZipFile) + zos = new ZipArchiveOutputStream(fos) + + val sourcePath = sourceDir.toPath + FileUtils.listFiles(sourceDir, null, true).asScala.foreach { file => + var in: FileInputStream = null + try { + val path = file.toPath + val entry = new ZipArchiveEntry(sourcePath.relativize(path).toString) + if (supportPosix) { + entry.setUnixMode( + permissionsToMode(Files.getPosixFilePermissions(path).asScala) + | (if (entry.getName.endsWith(".exe")) 0x1ED else 0x1A4)) + } else if (entry.getName.endsWith(".exe")) { + entry.setUnixMode(0x1ED) // 755 + } else { + entry.setUnixMode(0x1A4) // 644 + } + zos.putArchiveEntry(entry) + + in = new FileInputStream(file) + IOUtils.copy(in, zos) + zos.closeArchiveEntry() + } finally { + IOUtils.closeQuietly(in) + } + } + } finally { + IOUtils.closeQuietly(zos) + IOUtils.closeQuietly(fos) + } + } + + /** + * Unzip a file to the given directory + * + * @param file file to be unzipped + * @param targetDir target directory + */ + def unzip(file: File, targetDir: File): Unit = { + var zipFile: ZipFile = null + try { + targetDir.mkdirs() + zipFile = new ZipFile(file) + zipFile.getEntries.asScala.foreach { entry => + val targetFile = new File(targetDir, entry.getName) + + if (targetFile.exists()) { + logWarning( + s"Target file/directory $targetFile already exists. Skip it for now. " + + s"Make sure this is expected.") + } else { + if (entry.isDirectory) { + targetFile.mkdirs() + } else { + targetFile.getParentFile.mkdirs() + val input = zipFile.getInputStream(entry) + val output = new FileOutputStream(targetFile) + IOUtils.copy(input, output) + IOUtils.closeQuietly(input) + IOUtils.closeQuietly(output) + if (supportPosix) { + val permissions = modeToPermissions(entry.getUnixMode) + // When run in Unix system, permissions will be empty, thus skip + // setting the empty permissions (which will empty the previous permissions). + if (permissions.nonEmpty) { + Files.setPosixFilePermissions(targetFile.toPath, permissions.asJava) + } + } + } + } + } + } catch { + case e: Exception => logError("exception caught during decompression:" + e) + } finally { + ZipFile.closeQuietly(zipFile) + } + } + + /** + * Exits the JVM, trying to do it nicely, otherwise doing it nastily. + * + * @param status the exit status, zero for OK, non-zero for error + * @param maxDelayMillis the maximum delay in milliseconds + */ + def exit(status: Int, maxDelayMillis: Long) { + try { + logInfo(s"Utils.exit() with status: $status, maxDelayMillis: $maxDelayMillis") + + // setup a timer, so if nice exit fails, the nasty exit happens + val timer = new Timer() + timer.schedule(new TimerTask() { + + override def run() { + Runtime.getRuntime.halt(status) + } + }, maxDelayMillis) + // try to exit nicely + System.exit(status); + } catch { + // exit nastily if we have a problem + case _: Throwable => Runtime.getRuntime.halt(status) + } finally { + // should never get here + Runtime.getRuntime.halt(status) + } + } + + /** + * Exits the JVM, trying to do it nicely, wait 1 second + * + * @param status the exit status, zero for OK, non-zero for error + */ + def exit(status: Int): Unit = { + exit(status, 1000) + } + + /** + * Normalize the Spark version by taking the first three numbers. + * For example: + * x.y.z => x.y.z + * x.y.z.xxx.yyy => x.y.z + * x.y => x.y + * + * @param version the Spark version to normalize + * @return Normalized Spark version. + */ + def normalizeSparkVersion(version: String): String = { + version + .split('.') + .take(3) + .zipWithIndex + .map({ + case (element, index) => { + index match { + case 2 => element.split("\\D+").lift(0).getOrElse("") + case _ => element + } + } + }) + .mkString(".") + } + + /** + * Validates the normalized spark version by verifying: + * - Spark version starts with sparkMajorMinorVersionPrefix. + * - If ignoreSparkPatchVersion is + * - true: valid + * - false: check if the spark version is in supportedSparkVersions. + * @param ignoreSparkPatchVersion Ignore spark patch version. + * @param sparkVersion The spark version. + * @param normalizedSparkVersion: The normalized spark version. + * @param supportedSparkMajorMinorVersionPrefix The spark major and minor version to validate against. + * @param supportedSparkVersions The set of supported spark versions. + */ + def validateSparkVersions( + ignoreSparkPatchVersion: Boolean, + sparkVersion: String, + normalizedSparkVersion: String, + supportedSparkMajorMinorVersionPrefix: String, + supportedSparkVersions: Set[String]): Unit = { + if (!normalizedSparkVersion.startsWith(s"$supportedSparkMajorMinorVersionPrefix.")) { + throw new IllegalArgumentException( + s"Unsupported spark version used: '$sparkVersion'. " + + s"Normalized spark version used: '$normalizedSparkVersion'. " + + s"Supported spark major.minor version: '$supportedSparkMajorMinorVersionPrefix'.") + } else if (ignoreSparkPatchVersion) { + logWarning( + s"Ignoring spark patch version. Spark version used: '$sparkVersion'. " + + s"Normalized spark version used: '$normalizedSparkVersion'. " + + s"Spark major.minor prefix used: '$supportedSparkMajorMinorVersionPrefix'.") + } else if (!supportedSparkVersions(normalizedSparkVersion)) { + val supportedVersions = supportedSparkVersions.toSeq.sorted.mkString(", ") + throw new IllegalArgumentException( + s"Unsupported spark version used: '$sparkVersion'. " + + s"Normalized spark version used: '$normalizedSparkVersion'. " + + s"Supported versions: '$supportedVersions'." + + "Patch version can be ignored, use setting 'spark.dotnet.ignoreSparkPatchVersionCheck'" ) + } + } + + private[spark] def listZipFileEntries(file: File): Array[String] = { + var zipFile: ZipFile = null + try { + zipFile = new ZipFile(file) + zipFile.getEntries.asScala.map(_.getName).toArray + } finally { + ZipFile.closeQuietly(zipFile) + } + } + + private[this] def permissionsToMode(permissions: Set[PosixFilePermission]): Int = { + posixFilePermissions.foldLeft(0) { (mode, perm) => + (mode << 1) | (if (permissions.contains(perm)) 1 else 0) + } + } + + private[this] def modeToPermissions(mode: Int): Set[PosixFilePermission] = { + posixFilePermissions.zipWithIndex + .filter { case (_, i) => (mode & (0x100 >>> i)) != 0 } + .map(_._1) + .toSet + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/DotnetBackendHandlerTest.scala b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/DotnetBackendHandlerTest.scala new file mode 100644 index 00000000..7088537e --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/DotnetBackendHandlerTest.scala @@ -0,0 +1,68 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + + +package org.apache.spark.api.dotnet + +import Extensions._ +import org.junit.Assert._ +import org.junit.{After, Before, Test} + +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, DataInputStream, DataOutputStream} + +@Test +class DotnetBackendHandlerTest { + private var backend: DotnetBackend = _ + private var tracker: JVMObjectTracker = _ + private var handler: DotnetBackendHandler = _ + + @Before + def before(): Unit = { + backend = new DotnetBackend + tracker = new JVMObjectTracker + handler = new DotnetBackendHandler(backend, tracker) + } + + @After + def after(): Unit = { + backend.close() + } + + @Test + def shouldTrackCallbackClientWhenDotnetProcessConnected(): Unit = { + val message = givenMessage(m => { + val serDe = new SerDe(null) + m.writeBoolean(true) // static method + serDe.writeInt(m, 1) // processId + serDe.writeInt(m, 1) // threadId + serDe.writeString(m, "DotnetHandler") // class name + serDe.writeString(m, "connectCallback") // command (method) name + m.writeInt(2) // number of arguments + m.writeByte('c') // 1st argument type (string) + serDe.writeString(m, "127.0.0.1") // 1st argument value (host) + m.writeByte('i') // 2nd argument type (integer) + m.writeInt(0) // 2nd argument value (port) + }) + + val payload = handler.handleBackendRequest(message) + val reply = new DataInputStream(new ByteArrayInputStream(payload)) + + assertEquals( + "status code must be successful.", 0, reply.readInt()) + assertEquals('j', reply.readByte()) + assertEquals(1, reply.readInt()) + val trackingId = new String(reply.readNBytes(1), "UTF-8") + assertEquals("1", trackingId) + val client = tracker.get(trackingId).get.asInstanceOf[Option[CallbackClient]].orNull + assertEquals(classOf[CallbackClient], client.getClass) + } + + private def givenMessage(func: DataOutputStream => Unit): Array[Byte] = { + val buffer = new ByteArrayOutputStream() + func(new DataOutputStream(buffer)) + buffer.toByteArray + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/DotnetBackendTest.scala b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/DotnetBackendTest.scala new file mode 100644 index 00000000..445486bb --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/DotnetBackendTest.scala @@ -0,0 +1,39 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + + +package org.apache.spark.api.dotnet + +import org.junit.Assert._ +import org.junit.{After, Before, Test} + +import java.net.InetAddress + +@Test +class DotnetBackendTest { + private var backend: DotnetBackend = _ + + @Before + def before(): Unit = { + backend = new DotnetBackend + } + + @After + def after(): Unit = { + backend.close() + } + + @Test + def shouldNotResetCallbackClient(): Unit = { + // Specifying port = 0 to select port dynamically. + backend.setCallbackClient(InetAddress.getLoopbackAddress.toString, port = 0) + + assertTrue(backend.callbackClient.isDefined) + assertThrows(classOf[Exception], () => { + backend.setCallbackClient(InetAddress.getLoopbackAddress.toString, port = 0) + }) + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/Extensions.scala b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/Extensions.scala new file mode 100644 index 00000000..c6904403 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/Extensions.scala @@ -0,0 +1,20 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + + +package org.apache.spark.api.dotnet + +import java.io.DataInputStream + +private[dotnet] object Extensions { + implicit class DataInputStreamExt(stream: DataInputStream) { + def readNBytes(n: Int): Array[Byte] = { + val buf = new Array[Byte](n) + stream.readFully(buf) + buf + } + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/JVMObjectTrackerTest.scala b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/JVMObjectTrackerTest.scala new file mode 100644 index 00000000..43ae7900 --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/JVMObjectTrackerTest.scala @@ -0,0 +1,42 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.api.dotnet + +import org.junit.Test + +@Test +class JVMObjectTrackerTest { + + @Test + def shouldReleaseAllReferences(): Unit = { + val tracker = new JVMObjectTracker + val firstId = tracker.put(new Object) + val secondId = tracker.put(new Object) + val thirdId = tracker.put(new Object) + + tracker.clear() + + assert(tracker.get(firstId).isEmpty) + assert(tracker.get(secondId).isEmpty) + assert(tracker.get(thirdId).isEmpty) + } + + @Test + def shouldResetCounter(): Unit = { + val tracker = new JVMObjectTracker + val firstId = tracker.put(new Object) + val secondId = tracker.put(new Object) + + tracker.clear() + + val thirdId = tracker.put(new Object) + + assert(firstId.equals("1")) + assert(secondId.equals("2")) + assert(thirdId.equals("1")) + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/SerDeTest.scala b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/SerDeTest.scala new file mode 100644 index 00000000..6854035a --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/api/dotnet/SerDeTest.scala @@ -0,0 +1,373 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.api.dotnet + +import org.apache.spark.api.dotnet.Extensions._ +import org.apache.spark.sql.Row +import org.junit.Assert._ +import org.junit.{Before, Test} + +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, DataInputStream, DataOutputStream} +import java.sql.Date +import scala.collection.JavaConverters._ + +@Test +class SerDeTest { + private var serDe: SerDe = _ + private var tracker: JVMObjectTracker = _ + + @Before + def before(): Unit = { + tracker = new JVMObjectTracker + serDe = new SerDe(tracker) + } + + @Test + def shouldReadNull(): Unit = { + val input = givenInput(in => { + in.writeByte('n') + }) + + assertEquals(null, serDe.readObject(input)) + } + + @Test + def shouldThrowForUnsupportedTypes(): Unit = { + val input = givenInput(in => { + in.writeByte('_') + }) + + assertThrows(classOf[IllegalArgumentException], () => { + serDe.readObject(input) + }) + } + + @Test + def shouldReadInteger(): Unit = { + val input = givenInput(in => { + in.writeByte('i') + in.writeInt(42) + }) + + assertEquals(42, serDe.readObject(input)) + } + + @Test + def shouldReadLong(): Unit = { + val input = givenInput(in => { + in.writeByte('g') + in.writeLong(42) + }) + + assertEquals(42L, serDe.readObject(input)) + } + + @Test + def shouldReadDouble(): Unit = { + val input = givenInput(in => { + in.writeByte('d') + in.writeDouble(42.42) + }) + + assertEquals(42.42, serDe.readObject(input)) + } + + @Test + def shouldReadBoolean(): Unit = { + val input = givenInput(in => { + in.writeByte('b') + in.writeBoolean(true) + }) + + assertEquals(true, serDe.readObject(input)) + } + + @Test + def shouldReadString(): Unit = { + val payload = "Spark Dotnet" + val input = givenInput(in => { + in.writeByte('c') + in.writeInt(payload.getBytes("UTF-8").length) + in.write(payload.getBytes("UTF-8")) + }) + + assertEquals(payload, serDe.readObject(input)) + } + + @Test + def shouldReadMap(): Unit = { + val input = givenInput(in => { + in.writeByte('e') // map type descriptor + in.writeInt(3) // size + in.writeByte('i') // key type + in.writeInt(3) // number of keys + in.writeInt(11) // first key + in.writeInt(22) // second key + in.writeInt(33) // third key + in.writeInt(3) // number of values + in.writeByte('b') // first value type + in.writeBoolean(true) // first value + in.writeByte('d') // second value type + in.writeDouble(42.42) // second value + in.writeByte('n') // third type & value + }) + + assertEquals( + mapAsJavaMap(Map( + 11 -> true, + 22 -> 42.42, + 33 -> null)), + serDe.readObject(input)) + } + + @Test + def shouldReadEmptyMap(): Unit = { + val input = givenInput(in => { + in.writeByte('e') // map type descriptor + in.writeInt(0) // size + }) + + assertEquals(mapAsJavaMap(Map()), serDe.readObject(input)) + } + + @Test + def shouldReadBytesArray(): Unit = { + val input = givenInput(in => { + in.writeByte('r') // byte array type descriptor + in.writeInt(3) // length + in.write(Array[Byte](1, 2, 3)) // payload + }) + + assertArrayEquals(Array[Byte](1, 2, 3), serDe.readObject(input).asInstanceOf[Array[Byte]]) + } + + @Test + def shouldReadEmptyBytesArray(): Unit = { + val input = givenInput(in => { + in.writeByte('r') // byte array type descriptor + in.writeInt(0) // length + }) + + assertArrayEquals(Array[Byte](), serDe.readObject(input).asInstanceOf[Array[Byte]]) + } + + @Test + def shouldReadEmptyList(): Unit = { + val input = givenInput(in => { + in.writeByte('l') // type descriptor + in.writeByte('i') // element type + in.writeInt(0) // length + }) + + assertArrayEquals(Array[Int](), serDe.readObject(input).asInstanceOf[Array[Int]]) + } + + @Test + def shouldReadList(): Unit = { + val input = givenInput(in => { + in.writeByte('l') // type descriptor + in.writeByte('b') // element type + in.writeInt(3) // length + in.writeBoolean(true) + in.writeBoolean(false) + in.writeBoolean(true) + }) + + assertArrayEquals(Array(true, false, true), serDe.readObject(input).asInstanceOf[Array[Boolean]]) + } + + @Test + def shouldThrowWhenReadingListWithUnsupportedType(): Unit = { + val input = givenInput(in => { + in.writeByte('l') // type descriptor + in.writeByte('_') // unsupported element type + }) + + assertThrows(classOf[IllegalArgumentException], () => { + serDe.readObject(input) + }) + } + + @Test + def shouldReadDate(): Unit = { + val input = givenInput(in => { + val date = "2020-12-31" + in.writeByte('D') // type descriptor + in.writeInt(date.getBytes("UTF-8").length) // date string size + in.write(date.getBytes("UTF-8")) + }) + + assertEquals(Date.valueOf("2020-12-31"), serDe.readObject(input)) + } + + @Test + def shouldReadObject(): Unit = { + val trackingObject = new Object + tracker.put(trackingObject) + val input = givenInput(in => { + val objectIndex = "1" + in.writeByte('j') // type descriptor + in.writeInt(objectIndex.getBytes("UTF-8").length) // size + in.write(objectIndex.getBytes("UTF-8")) + }) + + assertSame(trackingObject, serDe.readObject(input)) + } + + @Test + def shouldThrowWhenReadingNonTrackingObject(): Unit = { + val input = givenInput(in => { + val objectIndex = "42" + in.writeByte('j') // type descriptor + in.writeInt(objectIndex.getBytes("UTF-8").length) // size + in.write(objectIndex.getBytes("UTF-8")) + }) + + assertThrows(classOf[NoSuchElementException], () => { + serDe.readObject(input) + }) + } + + @Test + def shouldReadSparkRows(): Unit = { + val input = givenInput(in => { + in.writeByte('R') // type descriptor + in.writeInt(2) // number of rows + in.writeInt(1) // number of elements in 1st row + in.writeByte('i') // type of 1st element in 1st row + in.writeInt(11) + in.writeInt(3) // number of elements in 2st row + in.writeByte('b') // type of 1st element in 2nd row + in.writeBoolean(true) + in.writeByte('d') // type of 2nd element in 2nd row + in.writeDouble(42.24) + in.writeByte('g') // type of 3nd element in 2nd row + in.writeLong(99) + }) + + assertEquals( + seqAsJavaList(Seq( + Row.fromSeq(Seq(11)), + Row.fromSeq(Seq(true, 42.24, 99)))), + serDe.readObject(input)) + } + + @Test + def shouldReadArrayOfObjects(): Unit = { + val input = givenInput(in => { + in.writeByte('O') // type descriptor + in.writeInt(2) // number of elements + in.writeByte('i') // type of 1st element + in.writeInt(42) + in.writeByte('b') // type of 2nd element + in.writeBoolean(true) + }) + + assertEquals(Seq(42, true), serDe.readObject(input).asInstanceOf[Seq[Any]]) + } + + @Test + def shouldWriteNull(): Unit = { + val in = whenOutput(out => { + serDe.writeObject(out, null) + serDe.writeObject(out, scala.runtime.BoxedUnit.UNIT) + }) + + assertEquals(in.readByte(), 'n') + assertEquals(in.readByte(), 'n') + assertEndOfStream(in) + } + + @Test + def shouldWriteString(): Unit = { + val sparkDotnet = "Spark Dotnet" + val in = whenOutput(out => { + serDe.writeObject(out, sparkDotnet) + }) + + assertEquals(in.readByte(), 'c') // object type + assertEquals(in.readInt(), sparkDotnet.length) // length + assertArrayEquals(in.readNBytes(sparkDotnet.length), sparkDotnet.getBytes("UTF-8")) + assertEndOfStream(in) + } + + @Test + def shouldWritePrimitiveTypes(): Unit = { + val in = whenOutput(out => { + serDe.writeObject(out, 42.24f.asInstanceOf[Object]) + serDe.writeObject(out, 42L.asInstanceOf[Object]) + serDe.writeObject(out, 42.asInstanceOf[Object]) + serDe.writeObject(out, true.asInstanceOf[Object]) + }) + + assertEquals(in.readByte(), 'd') + assertEquals(in.readDouble(), 42.24F, 0.000001) + assertEquals(in.readByte(), 'g') + assertEquals(in.readLong(), 42L) + assertEquals(in.readByte(), 'i') + assertEquals(in.readInt(), 42) + assertEquals(in.readByte(), 'b') + assertEquals(in.readBoolean(), true) + assertEndOfStream(in) + } + + @Test + def shouldWriteDate(): Unit = { + val date = "2020-12-31" + val in = whenOutput(out => { + serDe.writeObject(out, Date.valueOf(date)) + }) + + assertEquals(in.readByte(), 'D') // type + assertEquals(in.readInt(), 10) // size + assertArrayEquals(in.readNBytes(10), date.getBytes("UTF-8")) // content + } + + @Test + def shouldWriteCustomObjects(): Unit = { + val customObject = new Object + val in = whenOutput(out => { + serDe.writeObject(out, customObject) + }) + + assertEquals(in.readByte(), 'j') + assertEquals(in.readInt(), 1) + assertArrayEquals(in.readNBytes(1), "1".getBytes("UTF-8")) + assertSame(tracker.get("1").get, customObject) + } + + @Test + def shouldWriteArrayOfCustomObjects(): Unit = { + val payload = Array(new Object, new Object) + val in = whenOutput(out => { + serDe.writeObject(out, payload) + }) + + assertEquals(in.readByte(), 'l') // array type + assertEquals(in.readByte(), 'j') // type of element in array + assertEquals(in.readInt(), 2) // array length + assertEquals(in.readInt(), 1) // size of 1st element's identifiers + assertArrayEquals(in.readNBytes(1), "1".getBytes("UTF-8")) // identifier of 1st element + assertEquals(in.readInt(), 1) // size of 2nd element's identifier + assertArrayEquals(in.readNBytes(1), "2".getBytes("UTF-8")) // identifier of 2nd element + assertSame(tracker.get("1").get, payload(0)) + assertSame(tracker.get("2").get, payload(1)) + } + + private def givenInput(func: DataOutputStream => Unit): DataInputStream = { + val buffer = new ByteArrayOutputStream() + val out = new DataOutputStream(buffer) + func(out) + new DataInputStream(new ByteArrayInputStream(buffer.toByteArray)) + } + + private def whenOutput = givenInput _ + + private def assertEndOfStream (in: DataInputStream): Unit = { + assertEquals(-1, in.read()) + } +} diff --git a/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/util/dotnet/UtilsTest.scala b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/util/dotnet/UtilsTest.scala new file mode 100644 index 00000000..736aa20b --- /dev/null +++ b/src/spark/Flowthru.Spark.Jvm/src/test/scala/org/apache/spark/util/dotnet/UtilsTest.scala @@ -0,0 +1,82 @@ +/* + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + * See the LICENSE file in the project root for more information. + */ + +package org.apache.spark.util.dotnet + +import org.apache.spark.SparkConf +import org.apache.spark.internal.config.dotnet.Dotnet.DOTNET_IGNORE_SPARK_PATCH_VERSION_CHECK +import org.junit.Assert.{assertEquals, assertThrows} +import org.junit.Test + +@Test +class UtilsTest { + + @Test + def shouldIgnorePatchVersion(): Unit = { + val sparkVersion = "3.5.1" + val sparkMajorMinorVersionPrefix = "3.5" + val supportedSparkVersions = Set[String]("3.5.0") + + Utils.validateSparkVersions( + true, + sparkVersion, + Utils.normalizeSparkVersion(sparkVersion), + sparkMajorMinorVersionPrefix, + supportedSparkVersions) + } + + @Test + def shouldThrowForUnsupportedVersion(): Unit = { + val sparkVersion = "3.5.1" + val normalizedSparkVersion = Utils.normalizeSparkVersion(sparkVersion) + val sparkMajorMinorVersionPrefix = "3.5" + val supportedSparkVersions = Set[String]("3.5.0") + + val exception = assertThrows( + classOf[IllegalArgumentException], + () => { + Utils.validateSparkVersions( + false, + sparkVersion, + normalizedSparkVersion, + sparkMajorMinorVersionPrefix, + supportedSparkVersions) + }) + + assertEquals( + s"Unsupported spark version used: '$sparkVersion'. " + + s"Normalized spark version used: '$normalizedSparkVersion'. " + + s"Supported versions: '${supportedSparkVersions.toSeq.sorted.mkString(", ")}'." + + "Patch version can be ignored, use setting 'spark.dotnet.ignoreSparkPatchVersionCheck'", + + exception.getMessage) + } + + @Test + def shouldThrowForUnsupportedMajorMinorVersion(): Unit = { + val sparkVersion = "3.3.0" + val normalizedSparkVersion = Utils.normalizeSparkVersion(sparkVersion) + val sparkMajorMinorVersionPrefix = "3.5" + val supportedSparkVersions = Set[String]("3.5.0") + + val exception = assertThrows( + classOf[IllegalArgumentException], + () => { + Utils.validateSparkVersions( + false, + sparkVersion, + normalizedSparkVersion, + sparkMajorMinorVersionPrefix, + supportedSparkVersions) + }) + + assertEquals( + s"Unsupported spark version used: '$sparkVersion'. " + + s"Normalized spark version used: '$normalizedSparkVersion'. " + + s"Supported spark major.minor version: '$sparkMajorMinorVersionPrefix'.", + exception.getMessage) + } +} diff --git a/src/spark/Flowthru.Spark/Attributes.cs b/src/spark/Flowthru.Spark/Attributes.cs new file mode 100644 index 00000000..d5e2e2d6 --- /dev/null +++ b/src/spark/Flowthru.Spark/Attributes.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Flowthru.Spark +{ + /// + /// Base class for custom attributes that involve the Spark version. + /// + public abstract class VersionAttribute : Attribute + { + /// + /// Constructor for VersionAttribute class. + /// + /// Spark version + protected VersionAttribute(string version) + { + Version = new Version(version); + } + + /// + /// Returns the Spark version. + /// + public Version Version { get; } + } + + /// + /// Custom attribute to denote the Spark version in which an API is introduced. + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class SinceAttribute : VersionAttribute + { + /// + /// Constructor for SinceAttribute class. + /// + /// Spark version + public SinceAttribute(string version) + : base(version) + { + } + } + + /// + /// Custom attribute to denote the Spark version in which an API is removed. + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class RemovedAttribute : VersionAttribute + { + /// + /// Constructor for RemovedAttribute class. + /// + /// Spark version + public RemovedAttribute(string version) + : base(version) + { + } + } + + /// + /// Custom attribute to denote the Spark version in which an API is deprecated. + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class DeprecatedAttribute : VersionAttribute + { + /// + /// Constructor for DeprecatedAttribute class. + /// + /// Spark version + public DeprecatedAttribute(string version) + : base(version) + { + } + } + + /// + /// Custom attribute to denote that a class is a Udf Wrapper. + /// + [AttributeUsage(AttributeTargets.Class)] + internal sealed class UdfWrapperAttribute : Attribute + { + } +} diff --git a/src/spark/Flowthru.Spark/Broadcast.cs b/src/spark/Flowthru.Spark/Broadcast.cs new file mode 100644 index 00000000..8cacb35d --- /dev/null +++ b/src/spark/Flowthru.Spark/Broadcast.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using MessagePack; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Network; +using Flowthru.Spark.Services; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark +{ + /// + /// A broadcast variable. Broadcast variables allow the programmer to keep a read-only variable + /// cached on each machine rather than shipping a copy of it with tasks. They can be used, for + /// example, to give every node a copy of a large input dataset in an efficient manner. Spark + /// also attempts to distribute broadcast variables using efficient broadcast algorithms to + /// reduce communication cost. + /// + [Serializable] + public sealed class Broadcast + : IMessagePackSerializationCallbackReceiver, + IJvmObjectReferenceProvider + { + [NonSerialized] + private readonly string _path; + [NonSerialized] + private readonly JvmObjectReference _jvmObject; + + private readonly long _bid; + + internal Broadcast(SparkContext sc, T value) + { + _path = CreateTempFilePath(sc.GetConf()); + _jvmObject = CreateBroadcast(sc, value); + _bid = (long)_jvmObject.Invoke("id"); + } + + // Default constructor, needed for deserialization + internal Broadcast() + { + } + + public JvmObjectReference Reference => _jvmObject; + + /// + /// Get the broadcasted value. + /// + /// The broadcasted value + public T Value() + { + return (T)BroadcastRegistry.Get(_bid); + } + + /// + /// Asynchronously delete cached copies of this broadcast on the executors. + /// If the broadcast is used after this is called, it will need to be re-sent to each + /// executor. + /// + public void Unpersist() + { + _jvmObject.Invoke("unpersist"); + } + + /// + /// Delete cached copies of this broadcast on the executors. If the broadcast is used after + /// this is called, it will need to be re-sent to each executor. + /// + /// Whether to block until unpersisting has completed + public void Unpersist(bool blocking) + { + _jvmObject.Invoke("unpersist", blocking); + } + + /// + /// Destroy all data and metadata related to this broadcast variable. Use this with + /// caution; once a broadcast variable has been destroyed, it cannot be used again. + /// This method blocks until destroy has completed. + /// + public void Destroy() + { + _jvmObject.Invoke("destroy"); + File.Delete(_path); + } + + /// + /// Function that creates a temporary directory inside the given directory and returns the + /// absolute filepath of temporary file name in that directory. + /// + /// SparkConf object + /// Absolute filepath of the created random file + private string CreateTempFilePath(SparkConf conf) + { + var localDir = (string)conf.Reference.Jvm.CallStaticJavaMethod( + "org.apache.spark.util.Utils", + "getLocalDir", + conf); + string dir = Path.Combine(localDir, "sparkdotnet"); + Directory.CreateDirectory(dir); + return Path.Combine(dir, Path.GetRandomFileName()); + } + + /// + /// Function to create the Broadcast variable (org.apache.spark.broadcast.Broadcast) + /// + /// SparkContext object of type + /// Broadcast value of type object + /// Returns broadcast variable of type + private JvmObjectReference CreateBroadcast(SparkContext sc, T value) + { + var javaSparkContext = (JvmObjectReference)sc.Reference.Jvm.CallStaticJavaMethod( + "org.apache.spark.api.java.JavaSparkContext", + "fromSparkContext", + sc); + + Version version = SparkEnvironment.SparkVersion; + return (version.Major, version.Minor) switch + { + (2, 4) => CreateBroadcast_V2_4_X(javaSparkContext, sc, value), + (3, _) => CreateBroadcast_V2_4_X(javaSparkContext, sc, value), + _ => throw new NotSupportedException($"Spark {version} not supported.") + }; + } + + /// + /// Calls the necessary Spark functions to create org.apache.spark.broadcast.Broadcast + /// object for Spark versions 2.4.0 and above, and returns the JVMObjectReference object. + /// + /// Java Spark context object + /// SparkContext object + /// Broadcast value of type object + /// Returns broadcast variable of type + private JvmObjectReference CreateBroadcast_V2_4_X( + JvmObjectReference javaSparkContext, + SparkContext sc, + object value) + { + // Using SparkConf.Get() and passing default value of 'false' instead of using + // PythonUtils.getEncryptionEnabled as the latter is a changing API wrt different + // Spark versions. + bool encryptionEnabled = bool.Parse( + sc.GetConf().Get("spark.io.encryption.enabled", "false")); + JvmObjectReference _pythonBroadcast; + + // Spark in Databricks is different from OSS Spark and requires to pass the SparkContext object to setupBroadcast. + if (ConfigurationService.IsDatabricks) + { + _pythonBroadcast = (JvmObjectReference)javaSparkContext.Jvm.CallStaticJavaMethod( + "org.apache.spark.api.python.PythonRDD", + "setupBroadcast", + javaSparkContext, + _path); + } + else + { + _pythonBroadcast = (JvmObjectReference)javaSparkContext.Jvm.CallStaticJavaMethod( + "org.apache.spark.api.python.PythonRDD", + "setupBroadcast", + _path); + } + + if (encryptionEnabled) + { + var pair = (JvmObjectReference[])_pythonBroadcast.Invoke("setupEncryptionServer"); + + using (ISocketWrapper socket = SocketFactory.CreateSocket()) + { + socket.Connect( + IPAddress.Loopback, + (int)pair[0].Invoke("intValue"), // port number + (string)pair[1].Invoke("toString")); // secret + WriteToStream(value, socket.OutputStream); + } + _pythonBroadcast.Invoke("waitTillDataReceived"); + } + else + { + WriteToFile(value); + } + + return (JvmObjectReference)javaSparkContext.Invoke("broadcast", _pythonBroadcast); + } + + /// TODO: This is not performant in the case of Broadcast encryption as it writes to stream + /// only after serializing the whole value, instead of serializing and writing in chunks + /// like Python. + /// + /// Function to write the broadcast value into the stream. + /// + /// Broadcast value to be written to the stream + /// Stream to write value to + private void WriteToStream(object value, Stream stream) + { + using var ms = new MemoryStream(); + Dump(value, ms); + SerDe.Write(stream, ms.Length); + ms.WriteTo(stream); + // -1 length indicates to the receiving end that we're done. + SerDe.Write(stream, -1); + } + + /// + /// Function that creates a file in _path to store the broadcast value in the given path. + /// + /// Broadcast value to be written to the file + private void WriteToFile(object value) + { + using FileStream f = File.Create(_path); + Dump(value, f); + } + + /// + /// Function that serializes and stores the object passed to the given Stream. + /// + /// Serializable object + /// Stream to which the object is serialized + private void Dump(object value, Stream stream) => + BinarySerDe.Serialize(stream, value); + + /// + /// Serialization callback function that adds to the JvmBroadcastRegistry when the + /// Broadcast variable object is being serialized. + /// + public void OnBeforeSerialize() + { + JvmBroadcastRegistry.Add(_jvmObject); + } + + /// + /// Deserialization callback function + /// + public void OnAfterDeserialize() + { + } + } + + /// + /// Global registry to store the object value of all active broadcast variables from + /// the workers. This registry is only used on the worker side when Broadcast.Value() is called + /// through a UDF. + /// + internal static class BroadcastRegistry + { + private static readonly ConcurrentDictionary s_registry = + new ConcurrentDictionary(); + private static readonly ILoggerService s_logger = + LoggerServiceFactory.GetLogger(typeof(BroadcastRegistry)); + + /// + /// Function to add the value of the broadcast variable to s_registry. + /// + /// Id of the Broadcast variable object to add + /// Value of the Broadcast variable + internal static void Add(long bid, object value) + { + bool result = s_registry.TryAdd(bid, value); + if (!result) + { + s_logger.LogInfo($"Broadcast {bid} already exists in the registry."); + } + } + + /// + /// Function to remove the Broadcast variable from s_registry. + /// + /// Id of the Broadcast variable object to remove + internal static void Remove(long bid) + { + if (!s_registry.TryRemove(bid, out _)) + { + s_logger.LogWarn($"Trying to remove a broadcast {bid} that does not exist."); + } + } + + /// + /// Returns the value of the Broadcast variable object of given Id. + /// + /// Id of the Broadcast variable object + /// Value of the Broadcast variable with given Id + internal static object Get(long bid) => s_registry[bid]; + } + + /// + /// Stores the JVMObjectReference object of type org.apache.spark.broadcast.Broadcast for all + /// active broadcast variables that are sent to the workers through the CreatePythonFunction. + /// This registry is only used on the driver side. + /// + internal static class JvmBroadcastRegistry + { + private static ThreadLocal> s_jvmBroadcastVariables = + new ThreadLocal>(() => new List()); + + /// + /// Adds a JVMObjectReference object of type to the list. + /// + /// JVMObjectReference of the Broadcast variable + internal static void Add(JvmObjectReference broadcastJvmObject) => + s_jvmBroadcastVariables.Value.Add(broadcastJvmObject); + + /// + /// Clears s_jvmBroadcastVariables of all the JVMObjectReference objects of type + /// . + /// + internal static void Clear() => s_jvmBroadcastVariables.Value.Clear(); + + /// + /// Returns the static member s_jvmBroadcastVariables. + /// + /// A list of all broadcast objects of type + internal static List GetAll() => s_jvmBroadcastVariables.Value; + } +} diff --git a/src/spark/Flowthru.Spark/Constants.cs b/src/spark/Flowthru.Spark/Constants.cs new file mode 100644 index 00000000..2f148e83 --- /dev/null +++ b/src/spark/Flowthru.Spark/Constants.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Flowthru.Spark +{ + internal class Constants + { + internal const string RunningREPLEnvVar = "DOTNET_SPARK_RUNNING_REPL"; + } +} diff --git a/src/spark/Flowthru.Spark/Flowthru.Spark.csproj b/src/spark/Flowthru.Spark/Flowthru.Spark.csproj new file mode 100644 index 00000000..9fac7c4a --- /dev/null +++ b/src/spark/Flowthru.Spark/Flowthru.Spark.csproj @@ -0,0 +1,47 @@ + + + + net10.0 + Flowthru.Spark + true + + $(NoWarn);NU5104 + Flowthru fork of .NET for Apache Spark — driver-only JVM bridge and DataFrame API + flowthru;spark;apache-spark;dataframe + Flowthru.Spark + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/spark/Flowthru.Spark/Hadoop/Conf/Configuration.cs b/src/spark/Flowthru.Spark/Hadoop/Conf/Configuration.cs new file mode 100644 index 00000000..16aa62c4 --- /dev/null +++ b/src/spark/Flowthru.Spark/Hadoop/Conf/Configuration.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Hadoop.Conf +{ + /// + /// Provides access to configuration parameters. + /// + public class Configuration : IJvmObjectReferenceProvider + { + internal Configuration(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + } +} diff --git a/src/spark/Flowthru.Spark/Hadoop/Fs/FileSystem.cs b/src/spark/Flowthru.Spark/Hadoop/Fs/FileSystem.cs new file mode 100644 index 00000000..30325743 --- /dev/null +++ b/src/spark/Flowthru.Spark/Hadoop/Fs/FileSystem.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Flowthru.Spark.Hadoop.Conf; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Hadoop.Fs +{ + /// + /// A fairly generic filesystem. It may be implemented as a distributed filesystem, or as a "local" one + /// that reflects the locally-connected disk. The local version exists for small Hadoop instances and for + /// testing. + /// + /// All user code that may potentially use the Hadoop Distributed File System should be written to use a + /// FileSystem object. The Hadoop DFS is a multi-machine system that appears as a single disk. It's + /// useful because of its fault tolerance and potentially very large capacity. + /// + public class FileSystem : IJvmObjectReferenceProvider, IDisposable + { + internal FileSystem(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Returns the configured . + /// + /// The configuration to use. + /// The FileSystem. + public static FileSystem Get(Configuration conf) => + new FileSystem((JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + "org.apache.hadoop.fs.FileSystem", + "get", + conf)); + + /// + /// Delete a file. + /// + /// The path to delete. + /// If path is a directory and set to true, the directory is deleted else + /// throws an exception. In case of a file the recursive can be set to either true or false. + /// True if delete is successful else false. + public bool Delete(string path, bool recursive = true) + { + JvmObjectReference pathObject = + SparkEnvironment.JvmBridge.CallConstructor("org.apache.hadoop.fs.Path", path); + + return (bool)Reference.Invoke("delete", pathObject, recursive); + } + + /// + /// Check if a path exists. + /// + /// Source path + /// True if the path exists else false. + public bool Exists(string path) + { + JvmObjectReference pathObject = + SparkEnvironment.JvmBridge.CallConstructor("org.apache.hadoop.fs.Path", path); + + return (bool)Reference.Invoke("exists", pathObject); + } + + public void Dispose() => Reference.Invoke("close"); + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Internal/Dotnet/ArrayExtensions.cs b/src/spark/Flowthru.Spark/Interop/Internal/Dotnet/ArrayExtensions.cs new file mode 100644 index 00000000..b9148127 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Internal/Dotnet/ArrayExtensions.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Internal.Java.Util; + +namespace System +{ + /// + /// ArrayExtensions host custom extension methods for the + /// dotnet base class array T[]. + /// + public static class ArrayExtensions + { + /// + /// A custom extension method that helps transform from dotnet + /// array of type T to java.util.ArrayList. + /// + /// an array instance + /// elements type of param array + /// + internal static ArrayList ToJavaArrayList(this T[] array) + { + var arrayList = new ArrayList(SparkEnvironment.JvmBridge); + foreach (T item in array) + { + arrayList.Add(item); + } + return arrayList; + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Internal/Dotnet/DictionaryExtensions.cs b/src/spark/Flowthru.Spark/Interop/Internal/Dotnet/DictionaryExtensions.cs new file mode 100644 index 00000000..4f1645a4 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Internal/Dotnet/DictionaryExtensions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Internal.Java.Util; + +namespace System.Collections.Generic +{ + public static class Dictionary + { + /// + /// A custom extension method that helps transform from dotnet + /// Dictionary<string, string> to java.util.HashMap. + /// + /// a Dictionary instance + /// + internal static HashMap ToJavaHashMap(this Dictionary dictionary) + { + var hashMap = new HashMap(SparkEnvironment.JvmBridge); + foreach (KeyValuePair item in dictionary) + { + hashMap.Put(item.Key, item.Value); + } + return hashMap; + } + + /// + /// A custom extension method that helps transform from dotnet + /// Dictionary<string, object> to java.util.HashMap. + /// + /// a Dictionary instance + /// + internal static HashMap ToJavaHashMap(this Dictionary dictionary) + { + var hashMap = new HashMap(SparkEnvironment.JvmBridge); + foreach (KeyValuePair item in dictionary) + { + hashMap.Put(item.Key, item.Value); + } + return hashMap; + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/ArrayList.cs b/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/ArrayList.cs new file mode 100644 index 00000000..bf252fe6 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/ArrayList.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Interop.Internal.Java.Util +{ + /// + /// ArrayList class represents a java.util.ArrayList object. + /// + internal sealed class ArrayList : IJvmObjectReferenceProvider + { + + /// + /// Create a java.util.ArrayList JVM object + /// + /// JVM bridge to use + internal ArrayList(IJvmBridge jvm) + { + Reference = jvm.CallConstructor("java.util.ArrayList"); + } + + public JvmObjectReference Reference { get; private set; } + + internal void Add(object element) + { + Reference.Invoke("add", element); + } + + internal void AddAll(IEnumerable collection) + { + foreach (object elem in collection) + { + Add(elem); + } + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/HashMap.cs b/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/HashMap.cs new file mode 100644 index 00000000..6441eb25 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/HashMap.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Interop.Internal.Java.Util +{ + /// + /// HashMap class represents a java.util.HashMap object. + /// + internal sealed class HashMap : IJvmObjectReferenceProvider + { + /// + /// Create a java.util.HashMap JVM object + /// + /// JVM bridge to use + internal HashMap(IJvmBridge jvm) => + Reference = jvm.CallConstructor("java.util.HashMap"); + + public JvmObjectReference Reference { get; private set; } + + /// + /// Associates the specified value with the specified key in this map. + /// If the map previously contained a mapping for the key, the old value is replaced. + /// + /// key with which the specified value is to be associated + /// value to be associated with the specified key + internal void Put(object key, object value) => + Reference.Invoke("put", key, value); + + /// + /// Returns the value to which the specified key is mapped, + /// or null if this map contains no mapping for the key. + /// + /// value whose presence in this map is to be tested + /// value associated with the specified key + internal object Get(object key) => + Reference.Invoke("get", key); + + /// + /// Returns true if this map maps one or more keys to the specified value. + /// + /// The HashMap key + /// true if this map maps one or more keys to the specified value + internal bool ContainsValue(object value) => + (bool)Reference.Invoke("containsValue", value); + + /// + /// Returns an array of the keys contained in this map. + /// + /// An array of object hosting the keys contained in the map + internal object[] Keys() + { + var jvmObject = (JvmObjectReference)Reference.Invoke("keySet"); + var result = (object[])jvmObject.Invoke("toArray"); + return result; + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/Hashtable.cs b/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/Hashtable.cs new file mode 100644 index 00000000..c8c8e4a0 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/Hashtable.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Interop.Internal.Java.Util +{ + /// + /// Hashtable class represents a java.util.Hashtable object. + /// + internal sealed class Hashtable : IJvmObjectReferenceProvider + { + /// + /// Create a java.util.Hashtable JVM object + /// + /// JVM bridge to use + internal Hashtable(IJvmBridge jvm) => + Reference = jvm.CallConstructor("java.util.Hashtable"); + + public JvmObjectReference Reference { get; private set; } + + /// + /// Maps the specified key to the specified value in this Hashtable. + /// Neither the key nor the value can be null. + /// + /// The Hashtable key + /// The value + internal void Put(object key, object value) => + Reference.Invoke("put", key, value); + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/Properties.cs b/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/Properties.cs new file mode 100644 index 00000000..e727d5b9 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Internal/Java/Util/Properties.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Interop.Internal.Java.Util +{ + /// + /// Properties class represents a java.util.Properties object. + /// + internal class Properties : IJvmObjectReferenceProvider + { + /// + /// Create a java.util.Properties JVM object + /// + /// JVM bridge to use + internal Properties(IJvmBridge jvm) => + Reference = jvm.CallConstructor("java.util.Properties"); + + /// + /// Create a java.util.Properties JVM object and populate the entries + /// using + /// + /// JVM bridge to use + /// Dictionary used to populate the + /// java.util.Properties JVM object + internal Properties(IJvmBridge jvm, Dictionary properties) : this(jvm) + { + if (Reference != null) + { + foreach (KeyValuePair property in properties) + { + Reference.Invoke( + "setProperty", + property.Key, + property.Value); + } + } + } + + public JvmObjectReference Reference { get; private set; } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Internal/Scala/Option.cs b/src/spark/Flowthru.Spark/Interop/Internal/Scala/Option.cs new file mode 100644 index 00000000..540b7015 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Internal/Scala/Option.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Interop.Internal.Scala +{ + /// + /// Exposes subset of scala.Option[T] APIs. + /// + internal sealed class Option : IJvmObjectReferenceProvider + { + internal Option(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Returns true if the option is None, false otherwise. + /// + /// true if the option is None, false otherwise + internal bool IsEmpty() => (bool)Reference.Invoke("isEmpty"); + + /// + /// Returns true if the option is an instance of Some, false otherwise. + /// + /// true if the option is an instance of Some, false otherwise + internal bool IsDefined() => (bool)Reference.Invoke("isDefined"); + + /// + /// Returns the option's value as object type if the option is nonempty, + /// otherwise throws an exception on JVM side. + /// + /// object that this Option is referencing to + internal object Get() => Reference.Invoke("get"); + + /// + /// Returns the option's value if it is nonempty, or `null` if it is empty. + /// + /// object that this Option is referencing to + internal object OrNull() => IsDefined() ? Get() : null; + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Internal/Scala/Seq.cs b/src/spark/Flowthru.Spark/Interop/Internal/Scala/Seq.cs new file mode 100644 index 00000000..d83e03ef --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Internal/Scala/Seq.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Interop.Internal.Scala +{ + /// + /// Limited read-only implementation of Scala Seq[T] so that Seq objects can be read + /// into POCO collection types such as List. + /// + /// + internal sealed class Seq : IJvmObjectReferenceProvider, IEnumerable + { + internal Seq(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Size => (int)Reference.Invoke("size"); + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < Size; ++i) + { + yield return Apply(i); + } + } + + public T Apply(int index) => (T)Reference.Invoke("apply", index); + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/CallbackConnection.cs b/src/spark/Flowthru.Spark/Interop/Ipc/CallbackConnection.cs new file mode 100644 index 00000000..5ca008f2 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/CallbackConnection.cs @@ -0,0 +1,282 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using Flowthru.Spark.Network; +using Flowthru.Spark.Services; + +namespace Flowthru.Spark.Interop.Ipc +{ + /// + /// CallbackConnection is used to process the callback communication between + /// Dotnet and the JVM. It uses a TCP socket to communicate with the JVM side + /// and the socket is expected to be reused. + /// + internal sealed class CallbackConnection + { + private static readonly ILoggerService s_logger = + LoggerServiceFactory.GetLogger(typeof(CallbackConnection)); + + private readonly ISocketWrapper _socket; + + /// + /// Keeps track of all s by its Id. This is accessed + /// by the and the . + /// + private readonly ConcurrentDictionary _callbackHandlers; + + private volatile bool _isRunning = false; + + private int _numCallbacksRun = 0; + + internal CallbackConnection( + long connectionId, + ISocketWrapper socket, + ConcurrentDictionary callbackHandlers) + { + ConnectionId = connectionId; + _socket = socket; + _callbackHandlers = callbackHandlers; + + s_logger.LogInfo( + $"[{ConnectionId}] Connected with RemoteEndPoint: {socket.RemoteEndPoint}"); + } + + internal enum ConnectionStatus + { + /// + /// Connection is normal. + /// + OK, + + /// + /// Socket is closed by the JVM. + /// + SOCKET_CLOSED, + + /// + /// Request to close connection. + /// + REQUEST_CLOSE + } + + internal long ConnectionId { get; } + + internal bool IsRunning => _isRunning; + + /// + /// Run and start processing the callback connection. + /// + /// Cancellation token used to stop the connection. + internal void Run(CancellationToken token) + { + _isRunning = true; + Stream inputStream = _socket.InputStream; + Stream outputStream = _socket.OutputStream; + + token.Register(() => Stop()); + + try + { + while (_isRunning) + { + ConnectionStatus connectionStatus = + ProcessStream(inputStream, outputStream, out bool readComplete); + + if (connectionStatus == ConnectionStatus.OK) + { + outputStream.Flush(); + + ++_numCallbacksRun; + + // If the socket is not read through completely, then it cannot be reused. + if (!readComplete) + { + _isRunning = false; + + // Wait for server to complete to avoid 'connection reset' exception. + s_logger.LogInfo( + $"[{ConnectionId}] Sleep 500 millisecond to close socket."); + Thread.Sleep(500); + } + } + else if (connectionStatus == ConnectionStatus.REQUEST_CLOSE) + { + _isRunning = false; + s_logger.LogInfo( + $"[{ConnectionId}] Request to close connection received."); + } + else + { + _isRunning = false; + s_logger.LogWarn($"[{ConnectionId}] Socket is closed by JVM."); + } + } + } + catch (Exception e) + { + _isRunning = false; + s_logger.LogError($"[{ConnectionId}] Exiting with exception: {e}"); + } + finally + { + try + { + _socket.Dispose(); + } + catch (Exception e) + { + s_logger.LogWarn($"[{ConnectionId}] Exception while closing socket {e}"); + } + + s_logger.LogInfo( + $"[{ConnectionId}] Finished running {_numCallbacksRun} callback(s)."); + } + } + + private void Stop() + { + _isRunning = false; + s_logger.LogInfo($"[{ConnectionId}] Stopping CallbackConnection."); + } + + /// + /// Process the input and output streams. + /// + /// The input stream. + /// The output stream. + /// True if stream is read completely, false otherwise. + /// The connection status. + private ConnectionStatus ProcessStream( + Stream inputStream, + Stream outputStream, + out bool readComplete) + { + readComplete = false; + + try + { + byte[] requestFlagBytes = SerDe.ReadBytes(inputStream, sizeof(int)); + // For socket stream, read on the stream returns 0, which + // SerDe.ReadBytes() returns as null to denote the stream is closed. + if (requestFlagBytes == null) + { + return ConnectionStatus.SOCKET_CLOSED; + } + + // Check value of the initial request. Expected values are: + // - CallbackFlags.CLOSE + // - CallbackFlags.CALLBACK + int requestFlag = BinaryPrimitives.ReadInt32BigEndian(requestFlagBytes); + if (requestFlag == (int)CallbackFlags.CLOSE) { + return ConnectionStatus.REQUEST_CLOSE; + } + else if (requestFlag != (int)CallbackFlags.CALLBACK) + { + throw new Exception( + string.Format( + "Unexpected callback flag received. Expected: {0}, Received: {1}.", + CallbackFlags.CALLBACK, + requestFlag)); + } + + // Use callback id to get the registered handler. + int callbackId = SerDe.ReadInt32(inputStream); + if (!_callbackHandlers.TryGetValue( + callbackId, + out ICallbackHandler callbackHandler)) + { + throw new Exception($"Unregistered callback id: {callbackId}"); + } + + s_logger.LogInfo( + string.Format( + "[{0}] Received request for callback id: {1}, callback handler: {2}", + ConnectionId, + callbackId, + callbackHandler)); + + // Save contents of callback handler data to be used later. + using var callbackDataStream = + new MemoryStream(SerDe.ReadBytes(inputStream, SerDe.ReadInt32(inputStream))); + + // Check the end of stream. + int endOfStream = SerDe.ReadInt32(inputStream); + if (endOfStream == (int)CallbackFlags.END_OF_STREAM) + { + s_logger.LogDebug($"[{ConnectionId}] Received END_OF_STREAM signal."); + + // Run callback handler. + callbackHandler.Run(callbackDataStream); + + SerDe.Write(outputStream, (int)CallbackFlags.END_OF_STREAM); + readComplete = true; + } + else + { + // This may happen when the input data is not read completely. + s_logger.LogWarn( + $"[{ConnectionId}] Unexpected end of stream: {endOfStream}."); + + // Write flag to indicate the connection should be closed. + SerDe.Write(outputStream, (int)CallbackFlags.CLOSE); + } + + return ConnectionStatus.OK; + } + catch (Exception e) + { + s_logger.LogError($"[{ConnectionId}] ProcessStream() failed with exception: {e}"); + + try + { + SerDe.Write(outputStream, (int)CallbackFlags.DOTNET_EXCEPTION_THROWN); + SerDe.Write(outputStream, e.ToString()); + } + catch (IOException) + { + // JVM closed the socket. + } + catch (Exception ex) + { + s_logger.LogError( + $"[{ConnectionId}] Writing exception to stream failed with exception: {ex}"); + } + + throw; + } + } + } + + /// + /// Enums with which the Dotnet CallbackConnection communicates with + /// the JVM CallbackConnection. + /// + internal enum CallbackFlags : int + { + /// + /// Flag to indicate connection should be closed. + /// + CLOSE = -1, + + /// + /// Flag to indiciate callback should be called. + /// + CALLBACK = -2, + + /// + /// Flag to indicate an exception thrown from dotnet. + /// + DOTNET_EXCEPTION_THROWN = -3, + + /// + /// Flag to indicate end of stream. + /// + END_OF_STREAM = -4 + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/CallbackServer.cs b/src/spark/Flowthru.Spark/Interop/Ipc/CallbackServer.cs new file mode 100644 index 00000000..25ecc94c --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/CallbackServer.cs @@ -0,0 +1,272 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Threading; +using Flowthru.Spark.Network; +using Flowthru.Spark.Services; + +namespace Flowthru.Spark.Interop.Ipc +{ + /// + /// CallbackServer services callback requests from the JVM. + /// + internal sealed class CallbackServer + { + private static readonly ILoggerService s_logger = + LoggerServiceFactory.GetLogger(typeof(CallbackServer)); + + private readonly IJvmBridge _jvm; + + /// + /// Keeps track of all s by its Id. This is accessed + /// by the and the + /// running in the worker threads. + /// + private readonly ConcurrentDictionary _callbackHandlers = + new ConcurrentDictionary(); + + /// + /// Keeps track of all objects identified by its + /// . The main thread creates a + /// each time it receives a new socket connection + /// from the JVM side and inserts it into . Each worker + /// thread calls and removes the connection + /// once this call is finished. will not return + /// unless the needs to be closed. + /// Also, is used to bound the number of worker threads + /// since it gives you the total number of active s. + /// + private readonly ConcurrentDictionary _connections = + new ConcurrentDictionary(); + + /// + /// Each worker thread picks up a CallbackConnection from _waitingConnections + /// and runs it. + /// + private readonly BlockingCollection _waitingConnections = + new BlockingCollection(); + + /// + /// A used to notify threads that operations + /// should be canceled. + /// + private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); + + /// + /// Counter used to generate a unique id when registering a . + /// + private int _callbackCounter = 0; + + private bool _isRunning = false; + + private ISocketWrapper _listener; + + private JvmObjectReference _jvmCallbackClient; + + internal int CurrentNumConnections => _connections.Count; + + internal JvmObjectReference JvmCallbackClient + { + get + { + if (_jvmCallbackClient is null) + { + throw new InvalidOperationException( + "Please make sure that CallbackServer was started before accessing JvmCallbackClient."); + } + + return _jvmCallbackClient; + } + } + + internal CallbackServer(IJvmBridge jvm, bool run = true) + { + AppDomain.CurrentDomain.ProcessExit += (s, e) => Shutdown(); + _jvm = jvm; + + if (run) + { + Run(); + } + } + + /// + /// Produce a unique id and register a with it. + /// + /// The handler to register. + /// A unique id associated with the handler. + internal int RegisterCallback(ICallbackHandler callbackHandler) + { + int callbackId = Interlocked.Increment(ref _callbackCounter); + _callbackHandlers[callbackId] = callbackHandler; + + return callbackId; + } + + /// + /// Runs the callback server. + /// + /// The listening socket. + internal void Run(ISocketWrapper listener) + { + if (_isRunning) + { + s_logger.LogWarn("CallbackServer is already running."); + return; + } + + s_logger.LogInfo($"Starting CallbackServer."); + _isRunning = true; + + try + { + _listener = listener; + _listener.Listen(); + + // Communicate with the JVM the callback server's address and port. + var localEndPoint = (IPEndPoint)_listener.LocalEndPoint; + _jvmCallbackClient = (JvmObjectReference)_jvm.CallStaticJavaMethod( + "DotnetHandler", + "connectCallback", + localEndPoint.Address.ToString(), + localEndPoint.Port); + + s_logger.LogInfo($"Started CallbackServer on {localEndPoint}"); + + // Start accepting connections from JVM. + new Thread(() => StartServer(_listener)) + { + IsBackground = true + }.Start(); + } + catch (Exception e) + { + s_logger.LogError($"CallbackServer exiting with exception: {e}"); + Shutdown(); + } + } + + /// + /// Runs the callback server. + /// + private void Run() + { + Run(SocketFactory.CreateSocket()); + } + + /// + /// Starts listening to any connection from JVM. + /// + /// + private void StartServer(ISocketWrapper listener) + { + try + { + long connectionId = 1; + int numWorkerThreads = 0; + + while (_isRunning) + { + ISocketWrapper socket = listener.Accept(); + var connection = + new CallbackConnection(connectionId, socket, _callbackHandlers); + + _waitingConnections.Add(connection); + _connections[connectionId] = connection; + ++connectionId; + + int numConnections = CurrentNumConnections; + + // Start worker thread until there are at least as many worker threads + // as there are CallbackConnections. CallbackConnections are expected + // to stay open and reuse the socket to service repeated callback + // requests. However, if there is an issue with a connection, then + // CallbackConnection.Run will return, freeing up extra worker threads + // to service any _waitingConnections. + // + // For example, + // Assume there were 5 worker threads, each servicing a CallbackConnection + // (5 total healthy connections). If 2 CallbackConnection sockets closed + // unexpectedly, then there would be 5 worker threads and 3 healthy + // connections. If a new connection request arrived, then the + // CallbackConnection would be added to the _waitingConnections collection + // and no new worker threads would be started (2 worker threads are already + // waiting to take CallbackConnections from _waitingConnections). + while (numWorkerThreads < numConnections) + { + new Thread(RunWorkerThread) + { + IsBackground = true + }.Start(); + ++numWorkerThreads; + } + + s_logger.LogInfo( + $"Pool snapshot: [NumThreads:{numWorkerThreads}], " + + $"[NumConnections:{numConnections}]"); + } + } + catch (Exception e) + { + s_logger.LogError($"StartServer() exits with exception: {e}"); + Shutdown(); + } + } + + /// + /// is called for each worker thread when it starts. + /// doesn't return (except for the error cases), and + /// keeps pulling from and runs the retrieved + /// . + /// + private void RunWorkerThread() + { + try + { + while (_isRunning) + { + if (_waitingConnections.TryTake( + out CallbackConnection connection, + Timeout.Infinite)) + { + // The connection will only return when the connection is closing + // (via CancellationToken) or there are error cases. + connection.Run(_tokenSource.Token); + + // Assume the connection is in a bad state, and do not reuse it. + // Remove it from _connections list to prevent the server thread from + // creating more threads than needed. + _connections.TryRemove(connection.ConnectionId, out CallbackConnection _); + } + } + } + catch (Exception e) + { + s_logger.LogError($"RunWorkerThread() exits with an exception: {e}"); + Shutdown(); + } + } + + /// + /// Shuts down the by canceling any running threads + /// and disposing of resources. + /// + private void Shutdown() + { + s_logger.LogInfo("Shutting down CallbackServer"); + + _tokenSource.Cancel(); + _waitingConnections.Dispose(); + _connections.Clear(); + _callbackHandlers.Clear(); + _listener?.Dispose(); + _isRunning = false; + + _jvm.CallStaticJavaMethod("DotnetHandler", "closeCallback"); + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/ForeachBatchCallbackHandler.cs b/src/spark/Flowthru.Spark/Interop/Ipc/ForeachBatchCallbackHandler.cs new file mode 100644 index 00000000..742deccc --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/ForeachBatchCallbackHandler.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Streaming; + +namespace Flowthru.Spark.Interop.Ipc +{ + /// + /// callback handler. + /// + internal sealed class ForeachBatchCallbackHandler : ICallbackHandler + { + private readonly IJvmBridge _jvm; + + private readonly Action _func; + + internal ForeachBatchCallbackHandler(IJvmBridge jvm, Action func) + { + _jvm = jvm; + _func = func; + } + + public void Run(Stream inputStream) + { + var batchDf = + new DataFrame(new JvmObjectReference(SerDe.ReadString(inputStream), _jvm)); + long batchId = SerDe.ReadInt64(inputStream); + + _func(batchDf, batchId); + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/ICallbackHandler.cs b/src/spark/Flowthru.Spark/Interop/Ipc/ICallbackHandler.cs new file mode 100644 index 00000000..b2bae9df --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/ICallbackHandler.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; + +namespace Flowthru.Spark.Interop.Ipc +{ + /// + /// Interface for handling callbacks between the JVM and Dotnet. + /// + internal interface ICallbackHandler + { + void Run(Stream inputStream); + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/IJvmBridge.cs b/src/spark/Flowthru.Spark/Interop/Ipc/IJvmBridge.cs new file mode 100644 index 00000000..e19f4c2e --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/IJvmBridge.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Flowthru.Spark.Interop.Ipc +{ + /// + /// Interface of the bridge between JVM and CLR. + /// + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + public interface IJvmBridge : IDisposable + { + // Each method has three overloads: one argument, + // two arguments, and a params array of arguments. + // This covers the bulk of the call sites, avoiding + // params array allocations for the most common uses, + // which involve one, two, and zero arguments (the latter + // is covered by the compiler using Array.Empty with + // the params array overload). + + /// + /// Call java class constructor. + /// + /// The class name. + /// Parameter for the constructor. + /// The of the constructed class. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + JvmObjectReference CallConstructor( + string className, + object arg0); + + /// + /// Call java class constructor. + /// + /// The class name. + /// First parameter for the constructor. + /// Second parameter for the constructor. + /// The of the constructed class. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + JvmObjectReference CallConstructor( + string className, + object arg0, + object arg1); + + /// + /// Call java class constructor. + /// + /// The class name. + /// Parameters for the constructor. + /// The of the constructed class. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + JvmObjectReference CallConstructor( + string className, + params object[] args); + + /// + /// Call static java method. + /// + /// The class name. + /// Method name to invoke. + /// Parameter for the method. + /// The returned object of the method call. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + object CallStaticJavaMethod( + string className, + string methodName, + object arg0); + + /// + /// Call static java method. + /// + /// The class name. + /// Method name to invoke. + /// First parameter for the method. + /// Second parameter for the method. + /// The returned object of the method call. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + object CallStaticJavaMethod( + string className, + string methodName, + object arg0, + object arg1); + + /// + /// Call static java method. + /// + /// The class name. + /// Method name to invoke. + /// Parameters for the method. + /// The returned object of the method call. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + object CallStaticJavaMethod( + string className, + string methodName, + params object[] args); + + /// + /// Invokes a method on the JVM object that references to. + /// + /// + /// The on which to invoke the method. + /// + /// Method name to invoke. + /// Parameter for the method. + /// The returned object of the method call. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + object CallNonStaticJavaMethod( + JvmObjectReference jvmObject, + string methodName, + object arg0); + + /// + /// Invokes a method on the JVM object that references to. + /// + /// + /// The on which to invoke the method. + /// + /// Method name to invoke. + /// First parameter for the method. + /// Second parameter for the method. + /// The returned object of the method call. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + object CallNonStaticJavaMethod( + JvmObjectReference jvmObject, + string methodName, + object arg0, + object arg1); + + /// + /// Invokes a method on the JVM object that references to. + /// + /// + /// The on which to invoke the method. + /// + /// Method name to invoke. + /// Parameters for the method. + /// The returned object of the method call. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + object CallNonStaticJavaMethod( + JvmObjectReference jvmObject, + string methodName, + params object[] args); + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/IJvmBridgeFactory.cs b/src/spark/Flowthru.Spark/Interop/Ipc/IJvmBridgeFactory.cs new file mode 100644 index 00000000..7668d0fd --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/IJvmBridgeFactory.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Flowthru.Spark.Interop.Ipc +{ + internal interface IJvmBridgeFactory + { + IJvmBridge Create(int portNumber); + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/JsonSerDe.cs b/src/spark/Flowthru.Spark/Interop/Ipc/JsonSerDe.cs new file mode 100644 index 00000000..aa110906 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/JsonSerDe.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace Flowthru.Spark.Interop.Ipc +{ + /// + /// Json.NET Serialization/Deserialization helper class. + /// + internal static class JsonSerDe + { + /// Note: Scala side uses JSortedObject when parsing JSON, so the properties + /// in JObject need to be sorted. + /// + /// Extension method to sort items in a JSON object by keys. + /// + /// + /// + public static JObject SortProperties(this JObject jObject) + { + var sortedJObject = new JObject(); + foreach (JProperty property in jObject.Properties().OrderBy(p => p.Name)) + { + if (property.Value is JObject elem) + { + sortedJObject.Add(property.Name, elem.SortProperties()); + } + else if (property.Value is JArray arrayElem) + { + sortedJObject.Add(property.Name, arrayElem.SortProperties()); + } + else + { + sortedJObject.Add(property.Name, property.Value); + } + } + + return sortedJObject; + } + + /// + /// Extend method to sort items in a JSON array by keys. + /// + public static JArray SortProperties(this JArray jArray) + { + if (jArray.Count == 0) + { + return jArray; + } + + var sortedJArray = new JArray(); + foreach (JToken item in jArray) + { + if (item is JObject elem) + { + sortedJArray.Add(elem.SortProperties()); + } + else if (item is JArray arrayElem) + { + sortedJArray.Add(arrayElem.SortProperties()); + } + } + + return sortedJArray; + } + } + +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/JvmBridge.cs b/src/spark/Flowthru.Spark/Interop/Ipc/JvmBridge.cs new file mode 100644 index 00000000..95832bde --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/JvmBridge.cs @@ -0,0 +1,514 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using Flowthru.Spark.Network; +using Flowthru.Spark.Services; + +namespace Flowthru.Spark.Interop.Ipc +{ + /// + /// Implementation of thread safe IPC bridge between JVM and CLR + /// Using a concurrent socket connection queue (lightweight synchronization mechanism) + /// supporting async JVM calls like StreamingContext.AwaitTermination() + /// + internal sealed class JvmBridge : IJvmBridge + { + // TODO: On .NET Core 2.1, Span could be used with a stack-based + // two-object struct rather than having these thread-static arrays. + + [ThreadStatic] + private static object[] s_oneArgArray; + [ThreadStatic] + private static object[] s_twoArgArray; + [ThreadStatic] + private static MemoryStream s_payloadMemoryStream; + + private const int SocketBufferThreshold = 3; + private const int ThreadIdForRepl = 1; + + private readonly int _processId = Process.GetCurrentProcess().Id; + private readonly SemaphoreSlim _socketSemaphore; + private readonly ConcurrentQueue _sockets = + new ConcurrentQueue(); + private readonly ILoggerService _logger = + LoggerServiceFactory.GetLogger(typeof(JvmBridge)); + private readonly int _portNumber; + private readonly JvmThreadPoolGC _jvmThreadPoolGC; + private readonly bool _isRunningRepl; + + internal JvmBridge(int portNumber) + { + if (portNumber == 0) + { + throw new Exception("Port number is not set."); + } + + _portNumber = portNumber; + _logger.LogInfo($"JvMBridge port is {portNumber}"); + + _jvmThreadPoolGC = new JvmThreadPoolGC( + _logger, this, SparkEnvironment.ConfigurationService.JvmThreadGCInterval, _processId); + + _isRunningRepl = SparkEnvironment.ConfigurationService.IsRunningRepl(); + + int numBackendThreads = SparkEnvironment.ConfigurationService.GetNumBackendThreads(); + int maxNumSockets = numBackendThreads; + if (numBackendThreads >= (2 * SocketBufferThreshold)) + { + // Set the max number of concurrent sockets to be less than the number of + // JVM backend threads to allow some buffer. + maxNumSockets -= SocketBufferThreshold; + } + _logger.LogInfo($"The number of JVM backend thread is set to {numBackendThreads}. " + + $"The max number of concurrent sockets in JvmBridge is set to {maxNumSockets}."); + _socketSemaphore = new SemaphoreSlim(maxNumSockets, maxNumSockets); + } + + private ISocketWrapper GetConnection() + { + if (!_sockets.TryDequeue(out ISocketWrapper socket)) + { + socket = SocketFactory.CreateSocket(); + socket.Connect(IPAddress.Loopback, _portNumber); + } + + return socket; + } + + public JvmObjectReference CallConstructor(string className, object arg0) => + (JvmObjectReference)CallJavaMethod(isStatic: true, className, "", arg0); + + public JvmObjectReference CallConstructor(string className, object arg0, object arg1) => + (JvmObjectReference)CallJavaMethod(isStatic: true, className, "", arg0, arg1); + + public JvmObjectReference CallConstructor(string className, object[] args) => + (JvmObjectReference)CallJavaMethod(isStatic: true, className, "", args); + + public object CallStaticJavaMethod(string className, string methodName, object arg0) => + CallJavaMethod(isStatic: true, className, methodName, arg0); + + public object CallStaticJavaMethod( + string className, + string methodName, + object arg0, + object arg1) => + CallJavaMethod(isStatic: true, className, methodName, arg0, arg1); + + public object CallStaticJavaMethod(string className, string methodName, object[] args) => + CallJavaMethod(isStatic: true, className, methodName, args); + + public object CallNonStaticJavaMethod( + JvmObjectReference jvmObject, + string methodName, + object arg0) => + CallJavaMethod(isStatic: false, jvmObject, methodName, arg0); + + public object CallNonStaticJavaMethod( + JvmObjectReference jvmObject, + string methodName, + object arg0, + object arg1) => + CallJavaMethod(isStatic: false, jvmObject, methodName, arg0, arg1); + + public object CallNonStaticJavaMethod( + JvmObjectReference jvmObject, + string methodName, + object[] args) => + CallJavaMethod(isStatic: false, jvmObject, methodName, args); + + private object CallJavaMethod( + bool isStatic, + object classNameOrJvmObjectReference, + string methodName, + object arg0) + { + object[] oneArgArray = s_oneArgArray ??= new object[1]; + oneArgArray[0] = arg0; + + try + { + return CallJavaMethod( + isStatic, + classNameOrJvmObjectReference, + methodName, + oneArgArray); + } + finally + { + oneArgArray[0] = null; + } + } + + private object CallJavaMethod( + bool isStatic, + object classNameOrJvmObjectReference, + string methodName, + object arg0, + object arg1) + { + object[] twoArgArray = s_twoArgArray ??= new object[2]; + twoArgArray[0] = arg0; + twoArgArray[1] = arg1; + + try + { + return CallJavaMethod( + isStatic, + classNameOrJvmObjectReference, + methodName, + twoArgArray); + } + finally + { + twoArgArray[1] = null; + twoArgArray[0] = null; + } + } + + private object CallJavaMethod( + bool isStatic, + object classNameOrJvmObjectReference, + string methodName, + object[] args) + { + object returnValue = null; + ISocketWrapper socket = null; + + try + { + // Limit the number of connections to the JVM backend. Netty is configured + // to use a set number of threads to process incoming connections. Each + // new connection is delegated to these threads in a round robin fashion. + // A deadlock can occur on the JVM if a new connection is scheduled on a + // blocked thread. + _socketSemaphore.Wait(); + + // dotnet-interactive does not have a dedicated thread to process + // code submissions and each code submission can be processed in different + // threads. DotnetHandler uses the CLR thread id to ensure that the same + // JVM thread is used to handle the request, which means that code submitted + // through dotnet-interactive may be executed in different JVM threads. To + // mitigate this, when running in the REPL, submit requests to the DotnetHandler + // using the same thread id. This mitigation has some limitations in multithreaded + // scenarios. If a JVM method is blocking and needs a JVM method call issued by a + // separate thread to unblock it, then this scenario is not supported. + // + // ie, `StreamingQuery.AwaitTermination()` is a blocking call and requires + // `StreamingQuery.Stop()` to be called to unblock it. However, the `Stop` + // call will never run because DotnetHandler will assign the method call to + // run on the same thread that `AwaitTermination` is running on. + Thread thread = _isRunningRepl ? null : Thread.CurrentThread; + int threadId = thread == null ? ThreadIdForRepl : thread.ManagedThreadId; + MemoryStream payloadMemoryStream = s_payloadMemoryStream ??= new MemoryStream(); + payloadMemoryStream.Position = 0; + + PayloadHelper.BuildPayload( + payloadMemoryStream, + isStatic, + _processId, + threadId, + classNameOrJvmObjectReference, + methodName, + args); + + socket = GetConnection(); + + Stream outputStream = socket.OutputStream; + outputStream.Write( + payloadMemoryStream.GetBuffer(), + 0, + (int)payloadMemoryStream.Position); + outputStream.Flush(); + + if (thread != null) + { + _jvmThreadPoolGC.TryAddThread(thread); + } + + Stream inputStream = socket.InputStream; + int isMethodCallFailed = SerDe.ReadInt32(inputStream); + if (isMethodCallFailed != 0) + { + string jvmFullStackTrace = SerDe.ReadString(inputStream); + string errorMessage = BuildErrorMessage( + isStatic, + classNameOrJvmObjectReference, + methodName, + args); + _logger.LogError(errorMessage); + _logger.LogError(jvmFullStackTrace); + throw new Exception(errorMessage, new JvmException(jvmFullStackTrace)); + } + + char typeAsChar = Convert.ToChar(inputStream.ReadByte()); + switch (typeAsChar) // TODO: Add support for other types. + { + case 'n': + break; + case 'j': + returnValue = new JvmObjectReference(SerDe.ReadString(inputStream), this); + break; + case 'c': + returnValue = SerDe.ReadString(inputStream); + break; + case 'i': + returnValue = SerDe.ReadInt32(inputStream); + break; + case 'g': + returnValue = SerDe.ReadInt64(inputStream); + break; + case 'd': + returnValue = SerDe.ReadDouble(inputStream); + break; + case 'b': + returnValue = Convert.ToBoolean(inputStream.ReadByte()); + break; + case 'l': + returnValue = ReadCollection(inputStream); + break; + default: + // Convert typeAsChar to UInt32 because the char may be non-printable. + throw new NotSupportedException( + string.Format( + "Identifier for type 0x{0:X} not supported", + Convert.ToUInt32(typeAsChar))); + } + _sockets.Enqueue(socket); + } + catch (Exception e) + { + _logger.LogException(e); + + if (e.InnerException is JvmException) + { + // DotnetBackendHandler caught JVM exception and passed back to dotnet. + // We can reuse this connection. + if (socket != null) // Safety check + { + _sockets.Enqueue(socket); + } + } + else + { + if (e.InnerException is SocketException) + { + _logger.LogError( + "Scala worker abandoned the connection, likely fatal crash on Java side. \n" + + "Ensure Spark runs with sufficient memory."); + } + + // In rare cases we may hit the Netty connection thread deadlock. + // If max backend threads is 10 and we are currently using 10 active + // connections (0 in the _sockets queue). When we hit this exception, + // the socket?.Dispose() will not requeue this socket and we will release + // the semaphore. Then in the next thread (assuming the other 9 connections + // are still busy), a new connection will be made to the backend and this + // connection may be scheduled on the blocked Netty thread. + socket?.Dispose(); + } + + throw; + } + finally + { + _socketSemaphore.Release(); + } + + return returnValue; + } + + private string BuildErrorMessage( + bool isStatic, + object classNameOrJvmObjectReference, + string methodName, + object[] args) + { + var errorMessage = new StringBuilder("JVM method execution failed: "); + const string ConstructorFormat = "Constructor failed for class '{0}'"; + const string StaticMethodFormat = "Static method '{0}' failed for class '{1}'"; + const string NonStaticMethodFormat = "Nonstatic method '{0}' failed for class '{1}'"; + + try + { + if (isStatic) + { + if (methodName.Equals("")) // "" is hard-coded in DotnetBackend. + { + errorMessage.AppendFormat( + ConstructorFormat, + classNameOrJvmObjectReference); + } + else + { + errorMessage.AppendFormat( + StaticMethodFormat, + methodName, + classNameOrJvmObjectReference); + } + } + else + { + errorMessage.AppendFormat( + NonStaticMethodFormat, + methodName, + classNameOrJvmObjectReference); + } + + if (args.Length == 0) + { + errorMessage.Append(" when called with no arguments"); + } + else + { + errorMessage.AppendFormat( + " when called with {0} arguments ({1})", + args.Length, + GetArgsAsString(args)); + } + } + catch (Exception) + { + errorMessage.Append("Exception when converting building error message"); + } + + return errorMessage.ToString(); + } + + private string GetArgsAsString(IEnumerable args) + { + var argsString = new StringBuilder(); + + try + { + int index = 1; + foreach (object arg in args) + { + string argValue = "null"; + string argType = "null"; + if (arg != null) + { + argValue = arg.ToString(); + argType = arg.GetType().Name; + } + + argsString.AppendFormat( + "[Index={0}, Type={1}, Value={2}], ", + index++, + argType, + argValue); + } + } + catch (Exception) + { + argsString.Append("Exception when converting parameters to string"); + } + + return argsString.ToString(); + } + + private object ReadCollection(Stream s) + { + object returnValue; + char listItemTypeAsChar = Convert.ToChar(s.ReadByte()); + int numOfItemsInList = SerDe.ReadInt32(s); + switch (listItemTypeAsChar) + { + case 'c': + var strArray = new string[numOfItemsInList]; + for (int itemIndex = 0; itemIndex < numOfItemsInList; ++itemIndex) + { + strArray[itemIndex] = SerDe.ReadString(s); + } + returnValue = strArray; + break; + case 'i': + var intArray = new int[numOfItemsInList]; + for (int itemIndex = 0; itemIndex < numOfItemsInList; ++itemIndex) + { + intArray[itemIndex] = SerDe.ReadInt32(s); + } + returnValue = intArray; + break; + case 'g': + var longArray = new long[numOfItemsInList]; + for (int itemIndex = 0; itemIndex < numOfItemsInList; ++itemIndex) + { + longArray[itemIndex] = SerDe.ReadInt64(s); + } + returnValue = longArray; + break; + case 'd': + var doubleArray = new double[numOfItemsInList]; + for (int itemIndex = 0; itemIndex < numOfItemsInList; ++itemIndex) + { + doubleArray[itemIndex] = SerDe.ReadDouble(s); + } + returnValue = doubleArray; + break; + case 'A': + var doubleArrayArray = new double[numOfItemsInList][]; + for (int itemIndex = 0; itemIndex < numOfItemsInList; ++itemIndex) + { + doubleArrayArray[itemIndex] = ReadCollection(s) as double[]; + } + returnValue = doubleArrayArray; + break; + case 'b': + var boolArray = new bool[numOfItemsInList]; + for (int itemIndex = 0; itemIndex < numOfItemsInList; ++itemIndex) + { + boolArray[itemIndex] = Convert.ToBoolean(s.ReadByte()); + } + returnValue = boolArray; + break; + case 'r': + var byteArrayArray = new byte[numOfItemsInList][]; + for (int itemIndex = 0; itemIndex < numOfItemsInList; ++itemIndex) + { + int byteArrayLen = SerDe.ReadInt32(s); + byteArrayArray[itemIndex] = SerDe.ReadBytes(s, byteArrayLen); + } + returnValue = byteArrayArray; + break; + case 'j': + var jvmObjectReferenceArray = new JvmObjectReference[numOfItemsInList]; + for (int itemIndex = 0; itemIndex < numOfItemsInList; ++itemIndex) + { + string itemIdentifier = SerDe.ReadString(s); + jvmObjectReferenceArray[itemIndex] = + new JvmObjectReference(itemIdentifier, this); + } + returnValue = jvmObjectReferenceArray; + break; + default: + // convert listItemTypeAsChar to UInt32 because the char may be non-printable + throw new NotSupportedException( + string.Format("Identifier for list item type 0x{0:X} not supported", + Convert.ToUInt32(listItemTypeAsChar))); + } + return returnValue; + } + + public void Dispose() + { + _jvmThreadPoolGC.Dispose(); + while (_sockets.TryDequeue(out ISocketWrapper socket)) + { + if (socket != null) + { + socket.Dispose(); + } + } + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/JvmBridgeFactory.cs b/src/spark/Flowthru.Spark/Interop/Ipc/JvmBridgeFactory.cs new file mode 100644 index 00000000..59ef6dff --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/JvmBridgeFactory.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Flowthru.Spark.Interop.Ipc +{ + internal class JvmBridgeFactory : IJvmBridgeFactory + { + public IJvmBridge Create(int portNumber) + { + return new JvmBridge(portNumber); + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/JvmObjectReference.cs b/src/spark/Flowthru.Spark/Interop/Ipc/JvmObjectReference.cs new file mode 100644 index 00000000..8033b8ed --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/JvmObjectReference.cs @@ -0,0 +1,268 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Threading; +using Flowthru.Spark.Services; + +namespace Flowthru.Spark.Interop.Ipc +{ + /// + /// JvmObjectId represents the unique owner for a JVM object. + /// The reason for having another layer on top of string id is + /// so that JvmObjectReference can be copied. + /// + internal sealed class JvmObjectId + { + private static readonly ILoggerService s_logger = + LoggerServiceFactory.GetLogger(typeof(JvmObjectId)); + + /// + /// Constructor for JvmObjectId class. + /// + /// Unique identifier + /// JVM bridge object + internal JvmObjectId(string id, IJvmBridge jvm) + { + Id = id; + Jvm = jvm; + } + + ~JvmObjectId() + { + if (!Environment.HasShutdownStarted && (Id != null) && (Jvm != null)) + { + ThreadPool.QueueUserWorkItem(_ => RemoveId()); + } + } + + /// + /// An unique identifier for an object created on the JVM. + /// + internal string Id { get; } + + /// + /// JVM bridge object. + /// + internal IJvmBridge Jvm { get; } + + /// + /// Implicit conversion to string. + /// + /// JvmObjectId to convert from + public static implicit operator string(JvmObjectId jvmObjectId) => jvmObjectId.Id; + + /// + /// Returns the string version of this object which is the unique id + /// of the JVM object. + /// + /// Id of the JVM object + public override string ToString() => Id; + + /// + /// Checks if the given object is same as the current object by comparing the id. + /// + /// Other object to compare against + /// True if the other object is equal. + public override bool Equals(object obj) => + ReferenceEquals(this, obj) || + ((obj is JvmObjectId jvmObjectId) && Id.Equals(jvmObjectId.Id)); + + /// + /// Returns the hash code of the current object. + /// + /// The hash code of the current object + public override int GetHashCode() => Id.GetHashCode(); + + private void RemoveId() + { + Debug.Assert(Id != null); + Debug.Assert(Jvm != null); + + // This function is registered in the finalizer. If the JVM side is launched + // in a debug mode, it is possible that the JVM process already exited + // when this function runs, resulting in an Exception thrown. In this case, + // the exception is swallowed and logged. + // In the normal case, there will be no broken connection since JVM launches + // the .NET process and waits for it to exit. + try + { + Jvm.CallStaticJavaMethod("DotnetHandler", "rm", Id); + } + catch (Exception e) + { + s_logger.LogException(e); + } + } + } + + /// + /// Implemented by objects that contain a . + /// + public interface IJvmObjectReferenceProvider + { + /// + /// Gets the wrapped by the object. + /// + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + JvmObjectReference Reference { get; } + } + + /// + /// Reference to object created in JVM. + /// + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + public sealed class JvmObjectReference : IJvmObjectReferenceProvider + { + /// + /// The time when this reference was created. + /// + private readonly DateTime _creationTime; + + /// + /// Constructor for the JvmObjectReference class. + /// + /// Id for the JVM object + /// IJvmBridge instance that created the JVM object + internal JvmObjectReference(string id, IJvmBridge jvm) + { + if (id is null) + { + throw new ArgumentNullException("JvmReferenceId cannot be null."); + } + + Id = new JvmObjectId(id, jvm); + + _creationTime = DateTime.UtcNow; + } + + /// + /// Copy constructor. + /// + /// Other JvmObjectReference object to copy from. + internal JvmObjectReference(JvmObjectReference other) + { + Id = other.Id; + _creationTime = other._creationTime; + } + + /// + /// An unique identifier for an object created on the JVM. + /// + internal JvmObjectId Id { get; } + + /// + /// IJvmBridge instance that created the JVM object. + /// + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + public IJvmBridge Jvm => Id.Jvm; + + JvmObjectReference IJvmObjectReferenceProvider.Reference => this; + + /// + /// Invokes a method on the JVM object that this JvmObjectReference references to. + /// + /// Parameter for the method. + /// Method name to invoke + /// The returned object of the method call. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + public object Invoke(string methodName, object arg0) => + Jvm.CallNonStaticJavaMethod(this, methodName, arg0); + + /// + /// Invokes a method on the JVM object that this JvmObjectReference references to. + /// + /// First parameter for the method. + /// Second parameter for the method. + /// Method name to invoke + /// The returned object of the method call. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + public object Invoke(string methodName, object arg0, object arg1) => + Jvm.CallNonStaticJavaMethod(this, methodName, arg0, arg1); + + /// + /// Invokes a method on the JVM object that this JvmObjectReference references to. + /// + /// Method name to invoke + /// Parameters for the method + /// The returned object of the method call. + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + public object Invoke(string methodName, params object[] args) + { + return Jvm.CallNonStaticJavaMethod(this, methodName, args); + } + + /// + /// Returns the string version of this object which is the unique id + /// of the JVM object. + /// + /// Id of the JVM object + public override string ToString() + { + return Id.ToString(); + } + + /// + /// Checks if the given object is same as the current object by comparing the ids. + /// + /// Other object to compare against + /// True if the other object is equal. + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + return + (obj is IJvmObjectReferenceProvider provider) && + Id.Equals(provider.Reference.Id); + } + + /// + /// Returns the hash code of the current object. + /// + /// The hash code of the current object + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Gets the debug info on the JVM object that the current object refers to. + /// + /// The debug info of the JVM object + public string GetDebugInfo() + { + var classObject = (JvmObjectReference)Invoke("getClass"); + var className = (string)classObject.Invoke("getName"); + + return $"Java object reference id={Id}, type name={className}, creation time (UTC)={_creationTime:o}"; + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/JvmThreadPoolGC.cs b/src/spark/Flowthru.Spark/Interop/Ipc/JvmThreadPoolGC.cs new file mode 100644 index 00000000..e36b5d40 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/JvmThreadPoolGC.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using Flowthru.Spark.Services; + +namespace Flowthru.Spark.Interop.Ipc +{ + /// + /// In .NET for Apache Spark, we maintain a 1-to-1 mapping between .NET application threads + /// and corresponding JVM threads. When a .NET thread calls a Spark API, that call is executed + /// by its corresponding JVM thread. This functionality allows for multithreaded applications + /// with thread-local variables. + /// + /// This class keeps track of the .NET application thread lifecycle. When a .NET application + /// thread is no longer alive, this class submits an rmThread command to the JVM backend to + /// dispose of its corresponding JVM thread. All methods are thread-safe. + /// + internal class JvmThreadPoolGC : IDisposable + { + private readonly ILoggerService _loggerService; + private readonly IJvmBridge _jvmBridge; + private readonly TimeSpan _threadGCInterval; + private readonly int _processId; + private readonly ConcurrentDictionary _activeThreads; + + private readonly object _activeThreadGCTimerLock; + private Timer _activeThreadGCTimer; + + /// + /// Construct the JvmThreadPoolGC. + /// + /// Logger service. + /// The JvmBridge used to call JVM methods. + /// The interval to GC finished threads. + /// The ID of the process. + public JvmThreadPoolGC(ILoggerService loggerService, IJvmBridge jvmBridge, TimeSpan threadGCInterval, int processId) + { + _loggerService = loggerService; + _jvmBridge = jvmBridge; + _threadGCInterval = threadGCInterval; + _processId = processId; + _activeThreads = new ConcurrentDictionary(); + + _activeThreadGCTimerLock = new object(); + _activeThreadGCTimer = null; + } + + /// + /// Dispose of the GC timer and run a final round of thread GC. + /// + public void Dispose() + { + lock (_activeThreadGCTimerLock) + { + if (_activeThreadGCTimer != null) + { + _activeThreadGCTimer.Dispose(); + _activeThreadGCTimer = null; + } + } + + GCThreads(); + } + + /// + /// Try to start monitoring a thread. + /// + /// The thread to add. + /// True if success, false if already added. + public bool TryAddThread(Thread thread) + { + bool returnValue = _activeThreads.TryAdd(thread.ManagedThreadId, thread); + + // Initialize the GC timer if necessary. + if (_activeThreadGCTimer == null) + { + lock (_activeThreadGCTimerLock) + { + if (_activeThreadGCTimer == null && _activeThreads.Count > 0) + { + _activeThreadGCTimer = new Timer( + (state) => GCThreads(), + null, + _threadGCInterval, + _threadGCInterval); + } + } + } + + return returnValue; + } + + /// + /// Try to remove a thread from the pool. If the removal is successful, then the + /// corresponding JVM thread will also be disposed. + /// + /// The ID of the thread to remove. + /// True if success, false if the thread cannot be found. + private bool TryDisposeJvmThread(int threadId) + { + if (_activeThreads.TryRemove(threadId, out _)) + { + // _activeThreads does not have ownership of the threads on the .NET side. This + // class does not need to call Join() on the .NET Thread. However, this class is + // responsible for sending the rmThread command to the JVM to trigger disposal + // of the corresponding JVM thread. + if ((bool)_jvmBridge.CallStaticJavaMethod("DotnetHandler", "rmThread", _processId, threadId)) + { + _loggerService.LogDebug($"GC'd JVM thread {threadId}."); + return true; + } + else + { + _loggerService.LogWarn( + $"rmThread returned false for JVM thread {threadId}. " + + $"Either thread does not exist or has already been GC'd."); + } + } + + return false; + } + + /// + /// Remove any threads that are no longer active. + /// + private void GCThreads() + { + foreach (KeyValuePair kvp in _activeThreads) + { + if (!kvp.Value.IsAlive) + { + TryDisposeJvmThread(kvp.Key); + } + } + + lock (_activeThreadGCTimerLock) + { + // Dispose of the timer if there are no threads to monitor. + if (_activeThreadGCTimer != null && _activeThreads.IsEmpty) + { + _activeThreadGCTimer.Dispose(); + _activeThreadGCTimer = null; + } + } + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/PayloadHelper.cs b/src/spark/Flowthru.Spark/Interop/Ipc/PayloadHelper.cs new file mode 100644 index 00000000..2f8f3d62 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/PayloadHelper.cs @@ -0,0 +1,394 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.Interop.Ipc +{ + /// + /// Helper to build the IPC payload for JVM calls from CLR. + /// + internal class PayloadHelper + { + private static readonly byte[] s_int32TypeId = new[] { (byte)'i' }; + private static readonly byte[] s_int64TypeId = new[] { (byte)'g' }; + private static readonly byte[] s_stringTypeId = new[] { (byte)'c' }; + private static readonly byte[] s_boolTypeId = new[] { (byte)'b' }; + private static readonly byte[] s_doubleTypeId = new[] { (byte)'d' }; + private static readonly byte[] s_dateTypeId = new[] { (byte)'D' }; + private static readonly byte[] s_timestampTypeId = new[] { (byte)'t' }; + private static readonly byte[] s_jvmObjectTypeId = new[] { (byte)'j' }; + private static readonly byte[] s_byteArrayTypeId = new[] { (byte)'r' }; + private static readonly byte[] s_doubleArrayArrayTypeId = new[] { (byte)'A' }; + private static readonly byte[] s_arrayTypeId = new[] { (byte)'l' }; + private static readonly byte[] s_dictionaryTypeId = new[] { (byte)'e' }; + private static readonly byte[] s_rowArrTypeId = new[] { (byte)'R' }; + private static readonly byte[] s_objectArrTypeId = new[] { (byte)'O' }; + + private static readonly ConcurrentDictionary s_isDictionaryTable = + new ConcurrentDictionary(); + + internal static void BuildPayload( + MemoryStream destination, + bool isStaticMethod, + int processId, + int threadId, + object classNameOrJvmObjectReference, + string methodName, + object[] args) + { + // Reserve space for total length. + long originalPosition = destination.Position; + destination.Position += sizeof(int); + + SerDe.Write(destination, isStaticMethod); + SerDe.Write(destination, processId); + SerDe.Write(destination, threadId); + SerDe.Write(destination, classNameOrJvmObjectReference.ToString()); + SerDe.Write(destination, methodName); + SerDe.Write(destination, args.Length); + ConvertArgsToBytes(destination, args); + + // Write the length now that we've written out everything else. + long afterPosition = destination.Position; + destination.Position = originalPosition; + SerDe.Write(destination, (int)afterPosition - sizeof(int)); + destination.Position = afterPosition; + } + + internal static void ConvertArgsToBytes( + MemoryStream destination, + object[] args, + bool addTypeIdPrefix = true) + { + long posBeforeEnumerable, posAfterEnumerable; + int itemCount; + object[] convertArgs = null; + + foreach (object arg in args) + { + if (arg == null) + { + destination.WriteByte((byte)'n'); + continue; + } + + Type argType = arg.GetType(); + + if (addTypeIdPrefix) + { + SerDe.Write(destination, GetTypeId(argType)); + } + + switch (Type.GetTypeCode(argType)) + { + case TypeCode.Int32: + SerDe.Write(destination, (int)arg); + break; + + case TypeCode.Int64: + SerDe.Write(destination, (long)arg); + break; + + case TypeCode.String: + SerDe.Write(destination, (string)arg); + break; + + case TypeCode.Boolean: + SerDe.Write(destination, (bool)arg); + break; + + case TypeCode.Double: + SerDe.Write(destination, (double)arg); + break; + + case TypeCode.Object: + switch (arg) + { + case byte[] argByteArray: + SerDe.Write(destination, argByteArray.Length); + SerDe.Write(destination, argByteArray); + break; + + case int[] argInt32Array: + SerDe.Write(destination, s_int32TypeId); + SerDe.Write(destination, argInt32Array.Length); + foreach (int i in argInt32Array) + { + SerDe.Write(destination, i); + } + break; + + case long[] argInt64Array: + SerDe.Write(destination, s_int64TypeId); + SerDe.Write(destination, argInt64Array.Length); + foreach (long i in argInt64Array) + { + SerDe.Write(destination, i); + } + break; + + case double[] argDoubleArray: + SerDe.Write(destination, s_doubleTypeId); + SerDe.Write(destination, argDoubleArray.Length); + foreach (double d in argDoubleArray) + { + SerDe.Write(destination, d); + } + break; + + case double[][] argDoubleArrayArray: + SerDe.Write(destination, s_doubleArrayArrayTypeId); + SerDe.Write(destination, argDoubleArrayArray.Length); + foreach (double[] doubleArray in argDoubleArrayArray) + { + SerDe.Write(destination, doubleArray.Length); + foreach (double d in doubleArray) + { + SerDe.Write(destination, d); + } + } + break; + + case IEnumerable argByteArrayEnumerable: + SerDe.Write(destination, s_byteArrayTypeId); + posBeforeEnumerable = destination.Position; + destination.Position += sizeof(int); + itemCount = 0; + foreach (byte[] b in argByteArrayEnumerable) + { + ++itemCount; + SerDe.Write(destination, b.Length); + destination.Write(b, 0, b.Length); + } + posAfterEnumerable = destination.Position; + destination.Position = posBeforeEnumerable; + SerDe.Write(destination, itemCount); + destination.Position = posAfterEnumerable; + break; + + case IEnumerable argStringEnumerable: + SerDe.Write(destination, s_stringTypeId); + posBeforeEnumerable = destination.Position; + destination.Position += sizeof(int); + itemCount = 0; + foreach (string s in argStringEnumerable) + { + ++itemCount; + SerDe.Write(destination, s); + } + posAfterEnumerable = destination.Position; + destination.Position = posBeforeEnumerable; + SerDe.Write(destination, itemCount); + destination.Position = posAfterEnumerable; + break; + + case IEnumerable argJvmEnumerable: + SerDe.Write(destination, s_jvmObjectTypeId); + posBeforeEnumerable = destination.Position; + destination.Position += sizeof(int); + itemCount = 0; + foreach (IJvmObjectReferenceProvider jvmObject in argJvmEnumerable) + { + ++itemCount; + SerDe.Write(destination, jvmObject.Reference.Id); + } + posAfterEnumerable = destination.Position; + destination.Position = posBeforeEnumerable; + SerDe.Write(destination, itemCount); + destination.Position = posAfterEnumerable; + break; + + case IEnumerable argRowEnumerable: + posBeforeEnumerable = destination.Position; + destination.Position += sizeof(int); + itemCount = 0; + foreach (GenericRow r in argRowEnumerable) + { + ++itemCount; + SerDe.Write(destination, (int)r.Values.Length); + ConvertArgsToBytes(destination, r.Values, true); + } + posAfterEnumerable = destination.Position; + destination.Position = posBeforeEnumerable; + SerDe.Write(destination, itemCount); + destination.Position = posAfterEnumerable; + break; + + case IEnumerable argObjectEnumerable: + posBeforeEnumerable = destination.Position; + destination.Position += sizeof(int); + itemCount = 0; + if (convertArgs == null) + { + convertArgs = new object[1]; + } + foreach (object o in argObjectEnumerable) + { + ++itemCount; + convertArgs[0] = o; + ConvertArgsToBytes(destination, convertArgs, true); + } + posAfterEnumerable = destination.Position; + destination.Position = posBeforeEnumerable; + SerDe.Write(destination, itemCount); + destination.Position = posAfterEnumerable; + break; + + case var _ when IsDictionary(arg.GetType()): + // Generic dictionary, but we don't have it strongly typed as + // Dictionary + var dictInterface = (IDictionary)arg; + var dict = new Dictionary(dictInterface.Count); + IDictionaryEnumerator iter = dictInterface.GetEnumerator(); + while (iter.MoveNext()) + { + dict[iter.Key] = iter.Value; + } + + // Below serialization is corresponding to deserialization method + // ReadMap() of SerDe.scala. + + // dictionary's length + SerDe.Write(destination, dict.Count); + + // keys' data type + SerDe.Write( + destination, + GetTypeId(arg.GetType().GetGenericArguments()[0])); + // keys' length, same as dictionary's length + SerDe.Write(destination, dict.Count); + if (convertArgs == null) + { + convertArgs = new object[1]; + } + foreach (KeyValuePair kv in dict) + { + convertArgs[0] = kv.Key; + // keys, do not need type prefix. + ConvertArgsToBytes(destination, convertArgs, false); + } + + // values' length, same as dictionary's length + SerDe.Write(destination, dict.Count); + foreach (KeyValuePair kv in dict) + { + convertArgs[0] = kv.Value; + // values, need type prefix. + ConvertArgsToBytes(destination, convertArgs, true); + } + break; + + case IJvmObjectReferenceProvider argProvider: + SerDe.Write(destination, argProvider.Reference.Id); + break; + + case Date argDate: + SerDe.Write(destination, argDate.ToString()); + break; + + case Timestamp argTimestamp: + SerDe.Write(destination, argTimestamp.GetIntervalInSeconds()); + break; + + default: + throw new NotSupportedException( + string.Format($"Type {arg.GetType()} is not supported")); + } + break; + } + } + } + + internal static byte[] GetTypeId(Type type) + { + switch (Type.GetTypeCode(type)) + { + case TypeCode.Int32: + return s_int32TypeId; + case TypeCode.Int64: + return s_int64TypeId; + case TypeCode.String: + return s_stringTypeId; + case TypeCode.Boolean: + return s_boolTypeId; + case TypeCode.Double: + return s_doubleTypeId; + case TypeCode.Object: + if (typeof(IJvmObjectReferenceProvider).IsAssignableFrom(type)) + { + return s_jvmObjectTypeId; + } + + if (type == typeof(byte[])) + { + return s_byteArrayTypeId; + } + + if (type == typeof(int[]) || + type == typeof(long[]) || + type == typeof(double[]) || + type == typeof(double[][]) || + typeof(IEnumerable).IsAssignableFrom(type) || + typeof(IEnumerable).IsAssignableFrom(type)) + { + return s_arrayTypeId; + } + + if (IsDictionary(type)) + { + return s_dictionaryTypeId; + } + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return s_arrayTypeId; + } + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return s_rowArrTypeId; + } + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return s_objectArrTypeId; + } + + if (typeof(Date).IsAssignableFrom(type)) + { + return s_dateTypeId; + } + + if (typeof(Timestamp).IsAssignableFrom(type)) + { + return s_timestampTypeId; + } + break; + } + + // TODO: Support other types. + throw new NotSupportedException(string.Format("Type {0} not supported yet", type)); + } + + private static bool IsDictionary(Type type) + { + if (!s_isDictionaryTable.TryGetValue(type, out var isDictionary)) + { + s_isDictionaryTable[type] = isDictionary = + type.GetInterfaces().Any( + i => i.IsGenericType && + (i.GetGenericTypeDefinition() == typeof(IDictionary<,>))); + } + return isDictionary; + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/Ipc/SerDe.cs b/src/spark/Flowthru.Spark/Interop/Ipc/SerDe.cs new file mode 100644 index 00000000..fa4056f8 --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/Ipc/SerDe.cs @@ -0,0 +1,339 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers.Binary; +using System.IO; +using System.Text; + +namespace Flowthru.Spark.Interop.Ipc +{ + /// + /// Enums with which Worker communicates with Spark. + /// See spark/core/src/main/scala/org/apache/spark/api/python/PythonRunner.scala. + /// + internal enum SpecialLengths : int + { + /// + /// Flag to indicate the end of data section + /// + END_OF_DATA_SECTION = -1, + + /// + /// Flag to indicate an exception thrown from .NET side + /// + PYTHON_EXCEPTION_THROWN = -2, + + /// + /// Flag to indicate a timing data + /// + TIMING_DATA = -3, + + /// + /// Flag to indicate the end of stream + /// + END_OF_STREAM = -4, + + /// + /// Flag to indicate non-defined type + /// + NULL = -5, + + /// + /// Flag used by PySpark only. + /// + START_ARROW_STREAM = -6 + } + + // TODO: When targeting .NET Core 2.1+ or .NET Standard 2.1+, all of this code can be simplified + // using stackalloc'd spans and Stream.Read/Write(span). + + /// + /// Serialization and Deserialization of data types between JVM and CLR + /// + internal class SerDe + { + [ThreadStatic] + private static byte[] s_threadLocalBuffer; + + /// + /// Reads a boolean from a stream. + /// + /// The stream to read + /// The boolean value read from the stream + public static bool ReadBool(Stream s) => + Convert.ToBoolean(s.ReadByte()); + + /// + /// Reads an integer from a stream. + /// + /// The stream to be read + /// The integer read from stream + public static int ReadInt32(Stream s) + { + byte[] buffer = GetThreadLocalBuffer(sizeof(int)); + TryReadBytes(s, buffer, sizeof(int)); + return BinaryPrimitives.ReadInt32BigEndian(buffer); + } + + /// + /// Reads a long integer from a stream. + /// + /// The stream to be read + /// The long integer read from stream + public static long ReadInt64(Stream s) + { + byte[] buffer = GetThreadLocalBuffer(sizeof(long)); + TryReadBytes(s, buffer, sizeof(long)); + return BinaryPrimitives.ReadInt64BigEndian(buffer); + } + + /// + /// Reads a double from a stream. + /// + /// The stream to be read + /// The double read from stream + public static double ReadDouble(Stream s) + { + byte[] buffer = GetThreadLocalBuffer(sizeof(long)); + TryReadBytes(s, buffer, sizeof(long)); + return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(buffer)); + } + + /// + /// Reads a string from a stream + /// + /// The stream to be read + /// The string read from stream + public static string ReadString(Stream s) + { + byte[] buffer = GetThreadLocalBuffer(sizeof(int)); + if (!TryReadBytes(s, buffer, sizeof(int))) + { + return null; + } + + return ReadString(s, BinaryPrimitives.ReadInt32BigEndian(buffer)); + } + + /// + /// Reads a string with a given length from a stream + /// + /// The stream to be read + /// The length to be read + /// The string read from stream + public static string ReadString(Stream s, int length) + { + if (length == (int)SpecialLengths.NULL) + { + return null; + } + + byte[] buffer = GetThreadLocalBuffer(length); + TryReadBytes(s, buffer, length); + return Encoding.UTF8.GetString(buffer, 0, length); + } + + /// + /// Reads a byte array with a given length from a stream + /// + /// The stream to be read + /// The length to be read + /// The a byte array read from stream + /// + /// An ArgumentOutOfRangeException thrown if the given length is negative + /// + /// + /// An ArgumentException if the actual read length is less than the given length + /// + public static byte[] ReadBytes(Stream s, int length) + { + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length), length, "length can't be negative."); + } + + var buffer = new byte[length]; + if (length > 0) + { + int bytesRead; + int totalBytesRead = 0; + do + { + bytesRead = s.Read(buffer, totalBytesRead, length - totalBytesRead); + totalBytesRead += bytesRead; + } + while ((totalBytesRead < length) && (bytesRead > 0)); + + // The stream is closed, return null to notify function caller. + if (totalBytesRead == 0) + { + return null; + } + + if (totalBytesRead < length) + { + throw new ArgumentException( + $"Incomplete bytes read: {totalBytesRead}, expected: {length}"); + } + } + + return buffer; + } + + public static bool TryReadBytes(Stream s, byte[] buffer, int length) + { + if (length > 0) + { + int bytesRead; + int totalBytesRead = 0; + do + { + bytesRead = s.Read(buffer, totalBytesRead, length - totalBytesRead); + totalBytesRead += bytesRead; + } + while ((totalBytesRead < length) && (bytesRead > 0)); + + // The stream is closed, return false to notify function caller. + if (totalBytesRead == 0) + { + return false; + } + + if (totalBytesRead < length) + { + throw new ArgumentException( + $"Incomplete bytes read: {totalBytesRead}, expected: {length}"); + } + } + + return true; + } + + public static int? ReadBytesLength(Stream s) + { + byte[] lengthBuffer = ReadBytes(s, sizeof(int)); + if (lengthBuffer == null) + { + return null; + } + + int length = BinaryPrimitives.ReadInt32BigEndian(lengthBuffer); + if (length == (int)SpecialLengths.NULL) + { + return null; + } + + return length; + } + + /// + /// Reads a byte array from a stream. The first 4 bytes indicate the length of a byte array. + /// + /// The stream to be read + /// The byte array read from stream + public static byte[] ReadBytes(Stream s) + { + int? length = ReadBytesLength(s); + if (length == null) + { + return null; + } + + return ReadBytes(s, length.GetValueOrDefault()); + } + + private static byte[] GetThreadLocalBuffer(int minSize) + { + const int DefaultMinSize = 256; + + byte[] buffer = s_threadLocalBuffer; + if (buffer == null || buffer.Length < minSize) + { + s_threadLocalBuffer = buffer = new byte[Math.Max(DefaultMinSize, minSize)]; + } + + return buffer; + } + + /// + /// Writes a byte to a stream + /// + /// The stream to write + /// The byte to write + public static void Write(Stream s, byte value) => + s.WriteByte(value); + + /// + /// Writes a byte array to a stream + /// + /// The stream to write + /// The byte array to write + public static void Write(Stream s, byte[] value) => + Write(s, value, value.Length); + + /// + /// Writes a byte array to a stream + /// + /// The stream to write + /// The byte array to write + /// The number of bytes in the array to write. + public static void Write(Stream s, byte[] value, int count) => + s.Write(value, 0, count); + + /// + /// Writes a boolean to a stream + /// + /// The stream to write + /// The boolean value to write + public static void Write(Stream s, bool value) => + Write(s, Convert.ToByte(value)); + + /// + /// Writes an integer to a stream (big-endian). + /// + /// The stream to write + /// The integer to write + public static void Write(Stream s, int value) + { + byte[] buffer = GetThreadLocalBuffer(sizeof(int)); + BinaryPrimitives.WriteInt32BigEndian(buffer, value); + Write(s, buffer, sizeof(int)); + } + + /// + /// Writes a long integer to a stream (big-endian). + /// + /// The stream to write + /// The long integer to write + public static void Write(Stream s, long value) + { + byte[] buffer = GetThreadLocalBuffer(sizeof(long)); + BinaryPrimitives.WriteInt64BigEndian(buffer, value); + Write(s, buffer, sizeof(long)); + } + + /// + /// Writes a double to a stream (big-endian). + /// + /// The stream to write + /// The double to write + public static void Write(Stream s, double value) => + Write(s, BitConverter.DoubleToInt64Bits(value)); + + /// + /// Writes a string to a stream. + /// + /// The stream to write + /// The string to write + public static void Write(Stream s, string value) + { + byte[] buffer = GetThreadLocalBuffer( + sizeof(int) + Encoding.UTF8.GetMaxByteCount(value.Length)); + int len = Encoding.UTF8.GetBytes(value, 0, value.Length, buffer, sizeof(int)); + BinaryPrimitives.WriteInt32BigEndian(buffer, len); + Write(s, buffer, sizeof(int) + len); + } + } +} diff --git a/src/spark/Flowthru.Spark/Interop/SparkEnvironment.cs b/src/spark/Flowthru.Spark/Interop/SparkEnvironment.cs new file mode 100644 index 00000000..b79d1c4f --- /dev/null +++ b/src/spark/Flowthru.Spark/Interop/SparkEnvironment.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Services; + +namespace Flowthru.Spark.Interop +{ + /// + /// Contains everything needed to setup an environment for using .NET with Spark. + /// + public static class SparkEnvironment + { + private static readonly ILoggerService s_logger = + LoggerServiceFactory.GetLogger(typeof(SparkEnvironment)); + + private static Version GetSparkVersion() + { + var sparkVersion = new Version((string)JvmBridge.CallStaticJavaMethod( + "org.apache.spark.deploy.dotnet.DotnetRunner", + "SPARK_VERSION")); + + string sparkVersionOverride = + Environment.GetEnvironmentVariable("SPARK_VERSION_OVERRIDE"); + if (!string.IsNullOrEmpty(sparkVersionOverride)) + { + s_logger.LogInfo( + $"Overriding the Spark version from '{sparkVersion}' " + + $"to '{sparkVersionOverride}'."); + sparkVersion = new Version(sparkVersionOverride); + } + + return sparkVersion; + } + + private static readonly Lazy s_sparkVersion = + new Lazy(() => GetSparkVersion()); + internal static Version SparkVersion + { + get + { + return s_sparkVersion.Value; + } + } + + private static IJvmBridgeFactory s_jvmBridgeFactory; + internal static IJvmBridgeFactory JvmBridgeFactory + { + get + { + return s_jvmBridgeFactory ??= new JvmBridgeFactory(); + } + set + { + s_jvmBridgeFactory = value; + } + } + + private static IJvmBridge s_jvmBridge; + /// + /// The bridge between the JVM and the CLR. + /// + /// + /// This is exposed to help users interact with the JVM. It is provided with limited + /// support and should be used with caution. + /// + public static IJvmBridge JvmBridge + { + get + { + return s_jvmBridge ??= + JvmBridgeFactory.Create(ConfigurationService.GetBackendPortNumber()); + } + set + { + s_jvmBridge = value; + } + } + + private static IConfigurationService s_configurationService; + internal static IConfigurationService ConfigurationService + { + get + { + return s_configurationService ??= new ConfigurationService(); + } + set + { + s_configurationService = value; + } + } + + private static CallbackServer s_callbackServer; + internal static CallbackServer CallbackServer + { + get + { + return s_callbackServer ??= new CallbackServer(JvmBridge); + } + } + } +} diff --git a/src/spark/Flowthru.Spark/JvmException.cs b/src/spark/Flowthru.Spark/JvmException.cs new file mode 100644 index 00000000..fa9f84d4 --- /dev/null +++ b/src/spark/Flowthru.Spark/JvmException.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Flowthru.Spark +{ + /// + /// Contains the message returned from the on an error. + /// + public class JvmException : Exception + { + public JvmException(string message) + : base(message) + { + } + } +} diff --git a/src/spark/Flowthru.Spark/LICENSE-DOTNET-SPARK b/src/spark/Flowthru.Spark/LICENSE-DOTNET-SPARK new file mode 100644 index 00000000..ec713658 --- /dev/null +++ b/src/spark/Flowthru.Spark/LICENSE-DOTNET-SPARK @@ -0,0 +1,23 @@ +The MIT License (MIT) + +[Copyright (c) .NET Foundation and Contributors](https://github.com/dotnet/spark/blob/main/LICENSE) + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/spark/Flowthru.Spark/ML/Feature/Base.cs b/src/spark/Flowthru.Spark/ML/Feature/Base.cs new file mode 100644 index 00000000..a7952094 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/Base.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Reflection; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// Params is used for components that take parameters. This also provides + /// an internal param map to store parameter values attached to the instance. + /// An abstract class corresponds to scala's Params trait. + /// + public abstract class Params : Identifiable, IJvmObjectReferenceProvider + { + internal Params(string className) + : this(SparkEnvironment.JvmBridge.CallConstructor(className)) + { + } + + internal Params(string className, string uid) + : this(SparkEnvironment.JvmBridge.CallConstructor(className, uid)) + { + } + + internal Params(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Returns the JVM toString value rather than the .NET ToString default + /// + /// JVM toString() value + public override string ToString() => (string)Reference.Invoke("toString"); + + /// + /// The UID that was used to create the object. If no UID is passed in when creating the + /// object then a random UID is created when the object is created. + /// + /// string UID identifying the object + public string Uid() => (string)Reference.Invoke("uid"); + + /// + /// Returns a description of how a specific works and is currently set. + /// + /// The to explain + /// Description of the + public string ExplainParam(Param.Param param) => + (string)Reference.Invoke("explainParam", param); + + /// + /// Returns a description of how all of the 's that apply to this object + /// work and how they are currently set. + /// + /// Description of all the applicable 's + public string ExplainParams() => (string)Reference.Invoke("explainParams"); + + /// Checks whether a param is explicitly set. + /// The to be checked. + /// bool + public bool IsSet(Param.Param param) => (bool)Reference.Invoke("isSet", param); + + /// Checks whether a param is explicitly set or has a default value. + /// The to be checked. + /// bool + public bool IsDefined(Param.Param param) => (bool)Reference.Invoke("isDefined", param); + + /// + /// Tests whether this instance contains a param with a given name. + /// + /// The to be test. + /// bool + public bool HasParam(string paramName) => (bool)Reference.Invoke("hasParam", paramName); + + /// + /// Retrieves a so that it can be used to set the value of the + /// on the object. + /// + /// The name of the to get. + /// that can be used to set the actual value + public Param.Param GetParam(string paramName) => + new Param.Param((JvmObjectReference)Reference.Invoke("getParam", paramName)); + + /// + /// Sets the value of a specific . + /// + /// to set the value of + /// The value to use + /// The object that contains the newly set + public T Set(Param.Param param, object value) => + WrapAsType((JvmObjectReference)Reference.Invoke("set", param, value)); + + /// + /// Clears any value that was previously set for this . The value is + /// reset to the default value. + /// + /// The to set back to its original value + /// Object reference that was used to clear the + public T Clear(Param.Param param) => + WrapAsType((JvmObjectReference)Reference.Invoke("clear", param)); + + protected static T WrapAsType(JvmObjectReference reference) + { + ConstructorInfo constructor = typeof(T) + .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(c => + { + ParameterInfo[] parameters = c.GetParameters(); + return (parameters.Length == 1) && + (parameters[0].ParameterType == typeof(JvmObjectReference)); + }); + + return (T)constructor.Invoke(new object[] { reference }); + } + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/Bucketizer.cs b/src/spark/Flowthru.Spark/ML/Feature/Bucketizer.cs new file mode 100644 index 00000000..5b07b415 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/Bucketizer.cs @@ -0,0 +1,220 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// maps a column of continuous features to a column of feature + /// buckets. + /// + /// can map multiple columns at once by setting the inputCols + /// parameter. Note that when both the inputCol and inputCols parameters are set, an Exception + /// will be thrown. The splits parameter is only used for single column usage, and splitsArray + /// is for multiple columns. + /// + public class Bucketizer : + JavaModel, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.Bucketizer"; + + /// + /// Create a without any parameters + /// + public Bucketizer() : base(s_className) + { + } + + /// + /// Create a with a UID that is used to give the + /// a unique ID + /// + /// An immutable unique ID for the object and its derivatives. + public Bucketizer(string uid) : base(s_className, uid) + { + } + + internal Bucketizer(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Gets the splits that were set using SetSplits + /// + /// double[], the splits to be used to bucket the input column + public double[] GetSplits() => (double[])Reference.Invoke("getSplits"); + + /// + /// Split points for splitting a single column into buckets. To split multiple columns use + /// SetSplitsArray. You cannot use both SetSplits and SetSplitsArray at the same time + /// + /// + /// Split points for mapping continuous features into buckets. With n+1 splits, there are n + /// buckets. A bucket defined by splits x,y holds values in the range [x,y) except the last + /// bucket, which also includes y. The splits should be of length >= 3 and strictly + /// increasing. Values outside the splits specified will be treated as errors. + /// + /// New object + public Bucketizer SetSplits(double[] value) => + WrapAsBucketizer(Reference.Invoke("setSplits", value)); + + /// + /// Gets the splits that were set by SetSplitsArray + /// + /// double[][], the splits to be used to bucket the input columns + public double[][] GetSplitsArray() => (double[][])Reference.Invoke("getSplitsArray"); + + /// + /// Split points fot splitting multiple columns into buckets. To split a single column use + /// SetSplits. You cannot use both SetSplits and SetSplitsArray at the same time. + /// + /// + /// The array of split points for mapping continuous features into buckets for multiple + /// columns. For each input column, with n+1 splits, there are n buckets. A bucket defined + /// by splits x,y holds values in the range [x,y) except the last bucket, which also + /// includes y. The splits should be of length >= 3 and strictly increasing. + /// Values outside the splits specified will be treated as errors. + /// New object + public Bucketizer SetSplitsArray(double[][] value) => + WrapAsBucketizer(Reference.Invoke("setSplitsArray", (object)value)); + + /// + /// Gets the column that the should read from and convert into + /// buckets. This would have been set by SetInputCol + /// + /// string, the input column + public string GetInputCol() => (string)Reference.Invoke("getInputCol"); + + /// + /// Sets the column that the should read from and convert into + /// buckets + /// + /// The name of the column to as the source of the buckets + /// New object + public Bucketizer SetInputCol(string value) => + WrapAsBucketizer(Reference.Invoke("setInputCol", value)); + + /// + /// Gets the columns that should read from and convert into + /// buckets. This is set by SetInputCol + /// + /// IEnumerable<string>, list of input columns + public IEnumerable GetInputCols() => + ((string[])(Reference.Invoke("getInputCols"))).ToList(); + + /// + /// Sets the columns that should read from and convert into + /// buckets. + /// + /// Each column is one set of buckets so if you have two input columns you can have two + /// sets of buckets and two output columns. + /// + /// List of input columns to use as sources for buckets + /// New object + public Bucketizer SetInputCols(IEnumerable value) => + WrapAsBucketizer(Reference.Invoke("setInputCols", value)); + + /// + /// Gets the name of the column the output data will be written to. This is set by + /// SetInputCol + /// + /// string, the output column + public string GetOutputCol() => (string)Reference.Invoke("getOutputCol"); + + /// + /// The will create a new column in the DataFrame, this is the + /// name of the new column. + /// + /// The name of the new column which contains the bucket ID + /// New object + public Bucketizer SetOutputCol(string value) => + WrapAsBucketizer(Reference.Invoke("setOutputCol", value)); + + /// + /// The list of columns that the will create in the DataFrame. + /// This is set by SetOutputCols + /// + /// IEnumerable<string>, list of output columns + public IEnumerable GetOutputCols() => + ((string[])Reference.Invoke("getOutputCols")).ToList(); + + /// + /// The list of columns that the will create in the DataFrame. + /// + /// List of column names which will contain the bucket ID + /// New object + public Bucketizer SetOutputCols(List value) => + WrapAsBucketizer(Reference.Invoke("setOutputCols", value)); + + /// + /// Loads the that was previously saved using Save + /// + /// The path the previous was saved to + /// New object + public static Bucketizer Load(string path) => + WrapAsBucketizer( + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_className, "load", path)); + + /// + /// Executes the and transforms the DataFrame to include the new + /// column or columns with the bucketed data. + /// + /// The DataFrame to add the bucketed data to + /// + /// containing the original data and the new bucketed columns + /// + public override DataFrame Transform(DataFrame source) => + new DataFrame((JvmObjectReference)Reference.Invoke("transform", source)); + + /// + /// How should the handle invalid data, choices are "skip", + /// "error" or "keep" + /// + /// string showing the way Spark will handle invalid data + public string GetHandleInvalid() => (string)Reference.Invoke("getHandleInvalid"); + + /// + /// Tells the what to do with invalid data. + /// + /// Choices are "skip", "error" or "keep". Default is "error" + /// + /// "skip", "error" or "keep" + /// New object + public Bucketizer SetHandleInvalid(string value) => + WrapAsBucketizer(Reference.Invoke("setHandleInvalid", value.ToString())); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static Bucketizer WrapAsBucketizer(object obj) => + new Bucketizer((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/CountVectorizer.cs b/src/spark/Flowthru.Spark/ML/Feature/CountVectorizer.cs new file mode 100644 index 00000000..a63a1ba6 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/CountVectorizer.cs @@ -0,0 +1,220 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.ML.Feature +{ + public class CountVectorizer : + JavaEstimator, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.CountVectorizer"; + + /// + /// Creates a without any parameters. + /// + public CountVectorizer() : base(s_className) + { + } + + /// + /// Creates a with a UID that is used to give the + /// a unique ID. + /// + /// An immutable unique ID for the object and its derivatives. + public CountVectorizer(string uid) : base(s_className, uid) + { + } + + internal CountVectorizer(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// Fits a model to the input data. + /// The to fit the model to. + /// + public override CountVectorizerModel Fit(DataFrame dataFrame) => + new CountVectorizerModel((JvmObjectReference)Reference.Invoke("fit", dataFrame)); + + /// + /// Loads the that was previously saved using Save. + /// + /// + /// The path the previous was saved to. + /// + /// New object + public static CountVectorizer Load(string path) => + WrapAsCountVectorizer((JvmObjectReference) + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_className, "load", path)); + + /// + /// Gets the binary toggle to control the output vector values. If True, all nonzero counts + /// (after minTF filter applied) are set to 1. This is useful for discrete probabilistic + /// models that model binary events rather than integer counts. Default: false + /// + /// boolean + public bool GetBinary() => (bool)Reference.Invoke("getBinary"); + + /// + /// Sets the binary toggle to control the output vector values. If True, all nonzero counts + /// (after minTF filter applied) are set to 1. This is useful for discrete probabilistic + /// models that model binary events rather than integer counts. Default: false + /// + /// Turn the binary toggle on or off + /// with the new binary toggle value set + public CountVectorizer SetBinary(bool value) => + WrapAsCountVectorizer((JvmObjectReference)Reference.Invoke("setBinary", value)); + + /// + /// Gets the column that the should read from and convert + /// into buckets. This would have been set by SetInputCol. + /// + /// The input column of type string + public string GetInputCol() => (string)Reference.Invoke("getInputCol"); + + /// + /// Sets the column that the should read from. + /// + /// The name of the column to use as the source. + /// with the input column set + public CountVectorizer SetInputCol(string value) => + WrapAsCountVectorizer((JvmObjectReference)Reference.Invoke("setInputCol", value)); + + /// + /// Gets the name of the new column the creates in the + /// DataFrame. + /// + /// The name of the output column. + public string GetOutputCol() => (string)Reference.Invoke("getOutputCol"); + + /// + /// Sets the name of the new column the creates in the + /// DataFrame. + /// + /// The name of the output column which will be created. + /// New with the output column set + public CountVectorizer SetOutputCol(string value) => + WrapAsCountVectorizer((JvmObjectReference)Reference.Invoke("setOutputCol", value)); + + /// + /// Gets the maximum number of different documents a term could appear in to be included in + /// the vocabulary. A term that appears more than the threshold will be ignored. If this is + /// an integer greater than or equal to 1, this specifies the maximum number of documents + /// the term could appear in; if this is a double in [0,1), then this specifies the maximum + /// fraction of documents the term could appear in. + /// + /// The maximum document term frequency + [Since(Versions.V2_4_0)] + public double GetMaxDF() => (double)Reference.Invoke("getMaxDF"); + + /// + /// Sets the maximum number of different documents a term could appear in to be included in + /// the vocabulary. A term that appears more than the threshold will be ignored. If this is + /// an integer greater than or equal to 1, this specifies the maximum number of documents + /// the term could appear in; if this is a double in [0,1), then this specifies the maximum + /// fraction of documents the term could appear in. + /// + /// The maximum document term frequency + /// New with the max df value set + [Since(Versions.V2_4_0)] + public CountVectorizer SetMaxDF(double value) => + WrapAsCountVectorizer((JvmObjectReference)Reference.Invoke("setMaxDF", value)); + + /// + /// Gets the minimum number of different documents a term must appear in to be included in + /// the vocabulary. If this is an integer greater than or equal to 1, this specifies the + /// number of documents the term must appear in; if this is a double in [0,1), then this + /// specifies the fraction of documents. + /// + /// The minimum document term frequency + public double GetMinDF() => (double)Reference.Invoke("getMinDF"); + + /// + /// Sets the minimum number of different documents a term must appear in to be included in + /// the vocabulary. If this is an integer greater than or equal to 1, this specifies the + /// number of documents the term must appear in; if this is a double in [0,1), then this + /// specifies the fraction of documents. + /// + /// The minimum document term frequency + /// New with the min df value set + public CountVectorizer SetMinDF(double value) => + WrapAsCountVectorizer((JvmObjectReference)Reference.Invoke("setMinDF", value)); + + /// + /// Gets the filter to ignore rare words in a document. For each document, terms with + /// frequency/count less than the given threshold are ignored. If this is an integer + /// greater than or equal to 1, then this specifies a count (of times the term must appear + /// in the document); if this is a double in [0,1), then this specifies a fraction (out of + /// the document's token count). + /// + /// Note that the parameter is only used in transform of CountVectorizerModel and does not + /// affect fitting. + /// + /// Minimum term frequency + public double GetMinTF() => (double)Reference.Invoke("getMinTF"); + + /// + /// Sets the filter to ignore rare words in a document. For each document, terms with + /// frequency/count less than the given threshold are ignored. If this is an integer + /// greater than or equal to 1, then this specifies a count (of times the term must appear + /// in the document); if this is a double in [0,1), then this specifies a fraction (out of + /// the document's token count). + /// + /// Note that the parameter is only used in transform of CountVectorizerModel and does not + /// affect fitting. + /// + /// Minimum term frequency + /// New with the min term frequency set + public CountVectorizer SetMinTF(double value) => + WrapAsCountVectorizer((JvmObjectReference)Reference.Invoke("setMinTF", value)); + + /// + /// Gets the max size of the vocabulary. will build a + /// vocabulary that only considers the top vocabSize terms ordered by term frequency across + /// the corpus. + /// + /// The max size of the vocabulary of type int. + public int GetVocabSize() => (int)Reference.Invoke("getVocabSize"); + + /// + /// Sets the max size of the vocabulary. will build a + /// vocabulary that only considers the top vocabSize terms ordered by term frequency across + /// the corpus. + /// + /// The max vocabulary size + /// with the max vocab value set + public CountVectorizer SetVocabSize(int value) => + WrapAsCountVectorizer(Reference.Invoke("setVocabSize", value)); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static CountVectorizer WrapAsCountVectorizer(object obj) => + new CountVectorizer((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/CountVectorizerModel.cs b/src/spark/Flowthru.Spark/ML/Feature/CountVectorizerModel.cs new file mode 100644 index 00000000..5ecd59cc --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/CountVectorizerModel.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.ML.Feature +{ + public class CountVectorizerModel : + JavaModel, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.CountVectorizerModel"; + + /// + /// Creates a without any parameters + /// + /// The vocabulary to use + public CountVectorizerModel(List vocabulary) + : this(SparkEnvironment.JvmBridge.CallConstructor( + s_className, vocabulary)) + { + } + + /// + /// Creates a with a UID that is used to give the + /// a unique ID + /// + /// An immutable unique ID for the object and its derivatives. + /// The vocabulary to use + public CountVectorizerModel(string uid, List vocabulary) + : this(SparkEnvironment.JvmBridge.CallConstructor( + s_className, uid, vocabulary)) + { + } + + internal CountVectorizerModel(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Loads the that was previously saved using Save + /// + /// + /// The path the previous was saved to + /// + /// New object + public static CountVectorizerModel Load(string path) => + WrapAsCountVectorizerModel( + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_className, "load", path)); + + /// + /// Gets the binary toggle to control the output vector values. If True, all nonzero counts + /// (after minTF filter applied) are set to 1. This is useful for discrete probabilistic + /// models that model binary events rather than integer counts. Default: false + /// + /// Toggle value of type boolean + public bool GetBinary() => (bool)Reference.Invoke("getBinary"); + + /// + /// Sets the binary toggle to control the output vector values. If True, all nonzero counts + /// (after minTF filter applied) are set to 1. This is useful for discrete probabilistic + /// models that model binary events rather than integer counts. Default: false + /// + /// Turn the binary toggle on or off + /// + /// with the new binary toggle value set + /// + public CountVectorizerModel SetBinary(bool value) => + WrapAsCountVectorizerModel(Reference.Invoke("setBinary", value)); + + /// + /// Gets the column that the should read from and + /// convert into buckets. This would have been set by SetInputCol + /// + /// string, the input column + public string GetInputCol() => (string)Reference.Invoke("getInputCol"); + + /// + /// Sets the column that the should read from. + /// + /// The name of the column to use as the source. + /// with the input column set + public CountVectorizerModel SetInputCol(string value) => + WrapAsCountVectorizerModel(Reference.Invoke("setInputCol", value)); + + /// + /// Gets the name of the new column the will create in + /// the DataFrame. + /// + /// The name of the output column. + public string GetOutputCol() => (string)Reference.Invoke("getOutputCol"); + + /// + /// Sets the name of the new column the will create in + /// the DataFrame. + /// + /// The name of the output column which will be created. + /// New with the output column set + public CountVectorizerModel SetOutputCol(string value) => + WrapAsCountVectorizerModel(Reference.Invoke("setOutputCol", value)); + + /// + /// Gets the maximum number of different documents a term could appear in to be included in + /// the vocabulary. A term that appears more than the threshold will be ignored. If this is + /// an integer greater than or equal to 1, this specifies the maximum number of documents + /// the term could appear in; if this is a double in [0,1), then this specifies the maximum + /// fraction of documents the term could appear in. + /// + /// The maximum document term frequency of type double. + public double GetMaxDF() => (double)Reference.Invoke("getMaxDF"); + + /// + /// Gets the minimum number of different documents a term must appear in to be included in + /// the vocabulary. If this is an integer greater than or equal to 1, this specifies the + /// number of documents the term must appear in; if this is a double in [0,1), then this + /// specifies the fraction of documents. + /// + /// The minimum document term frequency + public double GetMinDF() => (double)Reference.Invoke("getMinDF"); + + /// + /// Gets the filter to ignore rare words in a document. For each document, terms with + /// frequency/count less than the given threshold are ignored. If this is an integer + /// greater than or equal to 1, then this specifies a count (of times the term must appear + /// in the document); if this is a double in [0,1), then this specifies a fraction (out of + /// the document's token count). + /// + /// Note that the parameter is only used in transform of CountVectorizerModel and does not + /// affect fitting. + /// + /// Minimum term frequency of type double. + public double GetMinTF() => (double)Reference.Invoke("getMinTF"); + + /// + /// Sets the filter to ignore rare words in a document. For each document, terms with + /// frequency/count less than the given threshold are ignored. If this is an integer + /// greater than or equal to 1, then this specifies a count (of times the term must appear + /// in the document); if this is a double in [0,1), then this specifies a fraction (out of + /// the document's token count). + /// + /// Note that the parameter is only used in transform of CountVectorizerModel and does not + /// affect fitting. + /// + /// Minimum term frequency of type double. + /// + /// New with the min term frequency set + /// + public CountVectorizerModel SetMinTF(double value) => + WrapAsCountVectorizerModel(Reference.Invoke("setMinTF", value)); + + /// + /// Gets the max size of the vocabulary. will build a + /// vocabulary that only considers the top vocabSize terms ordered by term frequency across + /// the corpus. + /// + /// The max size of the vocabulary of type int. + public int GetVocabSize() => (int)Reference.Invoke("getVocabSize"); + + /// + /// Check transform validity and derive the output schema from the input schema. + /// + /// This checks for validity of interactions between parameters during Transform and + /// raises an exception if any parameter value is invalid. + /// + /// Typical implementation should first conduct verification on schema change and parameter + /// validity, including complex parameter interaction checks. + /// + /// + /// The of the which will be transformed. + /// + /// + /// The of the output schema that would have been derived from the + /// input schema, if Transform had been called. + /// + public override StructType TransformSchema(StructType value) => + new StructType( + (JvmObjectReference)Reference.Invoke( + "transformSchema", + DataType.FromJson(Reference.Jvm, value.Json))); + + /// + /// Converts a DataFrame with a text document to a sparse vector of token counts. + /// + /// to transform + /// containing the original data and the counts + public override DataFrame Transform(DataFrame document) => + new DataFrame((JvmObjectReference)Reference.Invoke("transform", document)); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static CountVectorizerModel WrapAsCountVectorizerModel(object obj) => + new CountVectorizerModel((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/Estimator.cs b/src/spark/Flowthru.Spark/ML/Feature/Estimator.cs new file mode 100644 index 00000000..d994581e --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/Estimator.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Sql; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// A helper interface for JavaEstimator, so that when we have an array of JavaEstimators + /// with different type params, we can hold all of them with Estimator<object>. + /// + public interface IEstimator + { + M Fit(DataFrame dataset); + } + + /// + /// Abstract Class for estimators that fit models to data. + /// + /// + public abstract class JavaEstimator : JavaPipelineStage, IEstimator where M : JavaModel + { + internal JavaEstimator(string className) : base(className) + { + } + + internal JavaEstimator(string className, string uid) : base(className, uid) + { + } + + internal JavaEstimator(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Fits a model to the input data. + /// + /// input dataset. + /// fitted model + public virtual M Fit(DataFrame dataset) => + WrapAsType((JvmObjectReference)Reference.Invoke("fit", dataset)); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/Evaluator.cs b/src/spark/Flowthru.Spark/ML/Feature/Evaluator.cs new file mode 100644 index 00000000..a76008d9 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/Evaluator.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Sql; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// Abstract Class for evaluators that compute metrics from predictions. + /// + public abstract class JavaEvaluator : Params + { + internal JavaEvaluator(string className) : base(className) + { + } + + internal JavaEvaluator(string className, string uid) : base(className, uid) + { + } + + internal JavaEvaluator(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Evaluates model output and returns a scalar metric. + /// The value of isLargerBetter specifies whether larger values are better. + /// + /// a dataset that contains labels/observations and predictions. + /// metric + public virtual double Evaluate(DataFrame dataset) => + (double)Reference.Invoke("evaluate", dataset); + + /// + /// Indicates whether the metric returned by evaluate should be maximized + /// (true, default) or minimized (false). + /// A given evaluator may support multiple metrics which may be maximized or minimized. + /// + /// bool + public bool IsLargerBetter => + (bool)Reference.Invoke("isLargerBetter"); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/FeatureHasher.cs b/src/spark/Flowthru.Spark/ML/Feature/FeatureHasher.cs new file mode 100644 index 00000000..f8219832 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/FeatureHasher.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.ML.Feature +{ + public class FeatureHasher : + JavaTransformer, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.FeatureHasher"; + + /// + /// Creates a without any parameters. + /// + public FeatureHasher() : base(s_className) + { + } + + /// + /// Creates a with a UID that is used to give the + /// a unique ID. + /// + /// An immutable unique ID for the object and its derivatives. + public FeatureHasher(string uid) : base(s_className, uid) + { + } + + internal FeatureHasher(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Loads the that was previously saved using Save. + /// + /// + /// The path the previous was saved to. + /// + /// New object + public static FeatureHasher Load(string path) => + WrapAsFeatureHasher( + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_className, + "load", + path)); + + /// + /// Gets a list of the columns which have been specified as categorical columns. + /// + /// List of categorical columns, set by SetCategoricalCols + public IEnumerable GetCategoricalCols() => + (string[])Reference.Invoke("getCategoricalCols"); + + /// + /// Marks columns as categorical columns. + /// + /// List of column names to mark as categorical columns + /// New object + public FeatureHasher SetCategoricalCols(IEnumerable value) => + WrapAsFeatureHasher(Reference.Invoke("setCategoricalCols", value)); + + /// + /// Gets the columns that the should read from and convert into + /// hashes. This would have been set by SetInputCol. + /// + /// List of the input columns, set by SetInputCols + public IEnumerable GetInputCols() => (string[])Reference.Invoke("getInputCols"); + + /// + /// Sets the columns that the should read from and convert into + /// hashes. + /// + /// The name of the column to as use the source of the hash + /// New object + public FeatureHasher SetInputCols(IEnumerable value) => + WrapAsFeatureHasher(Reference.Invoke("setInputCols", value)); + + /// + /// Gets the number of features that should be used. Since a simple modulo is used to + /// transform the hash function to a column index, it is advisable to use a power of two + /// as the numFeatures parameter; otherwise the features will not be mapped evenly to the + /// columns. + /// + /// The number of features to be used + public int GetNumFeatures() => (int)Reference.Invoke("getNumFeatures"); + + /// + /// Sets the number of features that should be used. Since a simple modulo is used to + /// transform the hash function to a column index, it is advisable to use a power of two as + /// the numFeatures parameter; otherwise the features will not be mapped evenly to the + /// columns. + /// + /// int value of number of features + /// New object + public FeatureHasher SetNumFeatures(int value) => + WrapAsFeatureHasher(Reference.Invoke("setNumFeatures", value)); + + /// + /// Gets the name of the column the output data will be written to. This is set by + /// SetInputCol. + /// + /// string, the output column + public string GetOutputCol() => (string)Reference.Invoke("getOutputCol"); + + /// + /// Sets the name of the new column in the created by Transform. + /// + /// The name of the new column which will contain the hash + /// New object + public FeatureHasher SetOutputCol(string value) => + WrapAsFeatureHasher(Reference.Invoke("setOutputCol", value)); + + /// + /// Transforms the input . It is recommended that you validate that + /// the transform will succeed by calling TransformSchema. + /// + /// Input to transform + /// Transformed + public override DataFrame Transform(DataFrame value) => + new DataFrame((JvmObjectReference)Reference.Invoke("transform", value)); + + /// + /// Check transform validity and derive the output schema from the input schema. + /// + /// This checks for validity of interactions between parameters during Transform and + /// raises an exception if any parameter value is invalid. + /// + /// Typical implementation should first conduct verification on schema change and parameter + /// validity, including complex parameter interaction checks. + /// + /// + /// The of the which will be transformed. + /// + /// + /// The of the output schema that would have been derived from the + /// input schema, if Transform had been called. + /// + public override StructType TransformSchema(StructType value) => + new StructType( + (JvmObjectReference)Reference.Invoke( + "transformSchema", + DataType.FromJson(Reference.Jvm, value.Json))); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static FeatureHasher WrapAsFeatureHasher(object obj) => + new FeatureHasher((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/HashingTF.cs b/src/spark/Flowthru.Spark/ML/Feature/HashingTF.cs new file mode 100644 index 00000000..4c213e16 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/HashingTF.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// A Maps a sequence of terms to their term frequencies using the + /// hashing trick. Currently we use Austin Appleby's MurmurHash 3 algorithm + /// (MurmurHash3_x86_32) to calculate the hash code value for the term object. Since a simple + /// modulo is used to transform the hash function to a column index, it is advisable to use a + /// power of two as the numFeatures parameter; otherwise the features will not be mapped evenly + /// to the columns. + /// + public class HashingTF : + JavaTransformer, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.HashingTF"; + + /// + /// Create a without any parameters + /// + public HashingTF() : base(s_className) + { + } + + /// + /// Create a with a UID that is used to give the + /// a unique ID + /// + /// An immutable unique ID for the object and its derivatives. + public HashingTF(string uid) : base(s_className, uid) + { + } + + internal HashingTF(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Loads the that was previously saved using Save + /// + /// The path the previous was saved to + /// New object + public static HashingTF Load(string path) => + WrapAsHashingTF( + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_className, "load", path)); + + /// + /// Gets the binary toggle that controls term frequency counts + /// + /// Flag showing whether the binary toggle is on or off + public bool GetBinary() => (bool)Reference.Invoke("getBinary"); + + /// + /// Binary toggle to control term frequency counts. + /// If true, all non-zero counts are set to 1. This is useful for discrete probabilistic + /// models that model binary events rather than integer counts + /// + /// binary toggle, default is false + public HashingTF SetBinary(bool value) => + WrapAsHashingTF(Reference.Invoke("setBinary", value)); + + /// + /// Gets the column that the should read from + /// + /// string, the name of the input column + public string GetInputCol() => (string)Reference.Invoke("getInputCol"); + + /// + /// Sets the column that the should read from + /// + /// The name of the column to as the source + /// New object + public HashingTF SetInputCol(string value) => + WrapAsHashingTF(Reference.Invoke("setInputCol", value)); + + /// + /// The will create a new column in the , + /// this is the name of the new column. + /// + /// string, the name of the output col + public string GetOutputCol() => (string)Reference.Invoke("getOutputCol"); + + /// + /// The will create a new column in the , + /// this is the name of the new column. + /// + /// The name of the new column + /// New object + public HashingTF SetOutputCol(string value) => + WrapAsHashingTF(Reference.Invoke("setOutputCol", value)); + + /// + /// Gets the number of features that should be used. Since a simple modulo is used to + /// transform the hash function to a column index, it is advisable to use a power of two + /// as the numFeatures parameter; otherwise the features will not be mapped evenly to the + /// columns. + /// + /// The number of features to be used + public int GetNumFeatures() => (int)Reference.Invoke("getNumFeatures"); + + /// + /// Sets the number of features that should be used. Since a simple modulo is used to + /// transform the hash function to a column index, it is advisable to use a power of two as + /// the numFeatures parameter; otherwise the features will not be mapped evenly to the + /// columns. + /// + /// int + /// New object + public HashingTF SetNumFeatures(int value) => + WrapAsHashingTF(Reference.Invoke("setNumFeatures", value)); + + /// + /// Executes the and transforms the DataFrame to include the new + /// column or columns with the tokens. + /// + /// The to add the tokens to + /// containing the original data and the tokens + public override DataFrame Transform(DataFrame source) => + new DataFrame((JvmObjectReference)Reference.Invoke("transform", source)); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static HashingTF WrapAsHashingTF(object obj) => + new HashingTF((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/IDF.cs b/src/spark/Flowthru.Spark/ML/Feature/IDF.cs new file mode 100644 index 00000000..9dcd0bec --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/IDF.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// Inverse document frequency (IDF). The standard formulation is used: + /// idf = log((m + 1) / (d(t) + 1)), where m is the total number of documents and d(t) is + /// the number of documents that contain term t. + /// + /// This implementation supports filtering out terms which do not appear in a minimum number + /// of documents (controlled by the variable minDocFreq). For terms that are not in at least + /// minDocFreq documents, the IDF is found as 0, resulting in TF-IDFs of 0. + /// + public class IDF : + JavaEstimator, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = "org.apache.spark.ml.feature.IDF"; + + /// + /// Create a without any parameters + /// + public IDF() : base(s_className) + { + } + + /// + /// Create a with a UID that is used to give the + /// a unique ID + /// + /// An immutable unique ID for the object and its derivatives. + public IDF(string uid) : base(s_className, uid) + { + } + + internal IDF(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Gets the column that the should read from + /// + /// string, input column + public string GetInputCol() => (string)(Reference.Invoke("getInputCol")); + + /// + /// Sets the column that the should read from + /// + /// The name of the column to as the source + /// New object + public IDF SetInputCol(string value) => WrapAsIDF(Reference.Invoke("setInputCol", value)); + + /// + /// The will create a new column in the DataFrame, this is the + /// name of the new column. + /// + /// string, the output column + public string GetOutputCol() => (string)(Reference.Invoke("getOutputCol")); + + /// + /// The will create a new column in the DataFrame, this is the + /// name of the new column. + /// + /// The name of the new column + /// New object + public IDF SetOutputCol(string value) => + WrapAsIDF(Reference.Invoke("setOutputCol", value)); + + /// + /// Minimum of documents in which a term should appear for filtering + /// + /// int, minimum number of documents in which a term should appear + public int GetMinDocFreq() => (int)Reference.Invoke("getMinDocFreq"); + + /// + /// Minimum of documents in which a term should appear for filtering + /// + /// int, the minimum of documents a term should appear in + /// New object + public IDF SetMinDocFreq(int value) => + WrapAsIDF(Reference.Invoke("setMinDocFreq", value)); + + /// + /// Fits a model to the input data. + /// + /// The to fit the model to + /// New object + public override IDFModel Fit(DataFrame source) => + new IDFModel((JvmObjectReference)Reference.Invoke("fit", source)); + + /// + /// Loads the that was previously saved using Save + /// + /// The path the previous was saved to + /// New object, loaded from path + public static IDF Load(string path) + { + return WrapAsIDF( + SparkEnvironment.JvmBridge.CallStaticJavaMethod(s_className, "load", path)); + } + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static IDF WrapAsIDF(object obj) => new IDF((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/IDFModel.cs b/src/spark/Flowthru.Spark/ML/Feature/IDFModel.cs new file mode 100644 index 00000000..d28e1aa7 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/IDFModel.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// A that converts the input string to lowercase and then splits it by + /// white spaces. + /// + public class IDFModel : + JavaModel, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.IDFModel"; + + /// + /// Create a without any parameters + /// + public IDFModel() : base(s_className) + { + } + + /// + /// Create a with a UID that is used to give the + /// a unique ID + /// + /// An immutable unique ID for the object and its derivatives. + public IDFModel(string uid) : base(s_className, uid) + { + } + + internal IDFModel(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Gets the column that the should read from + /// + /// string, input column + public string GetInputCol() => (string)(Reference.Invoke("getInputCol")); + + /// + /// Sets the column that the should read from and convert into + /// buckets + /// + /// The name of the column to as the source + /// New object + public IDFModel SetInputCol(string value) => + WrapAsIDFModel(Reference.Invoke("setInputCol", value)); + + /// + /// The will create a new column in the , + /// this is the name of the new column. + /// + /// string, the output column + public string GetOutputCol() => (string)(Reference.Invoke("getOutputCol")); + + /// + /// The will create a new column in the DataFrame, this is the + /// name of the new column. + /// + /// The name of the new column which contains the tokens + /// + /// New object + public IDFModel SetOutputCol(string value) => + WrapAsIDFModel(Reference.Invoke("setOutputCol", value)); + + /// + /// Minimum of documents in which a term should appear for filtering + /// + /// Minimum number of documents a term should appear + public int GetMinDocFreq() => (int)Reference.Invoke("getMinDocFreq"); + + /// + /// Executes the and transforms the to + /// include the new column or columns with the tokens. + /// + /// The to add the tokens to + /// containing the original data and the tokens + public override DataFrame Transform(DataFrame source) => + new DataFrame((JvmObjectReference)Reference.Invoke("transform", source)); + + /// + /// Loads the that was previously saved using Save + /// + /// The path the previous was saved to + /// New object, loaded from path + public static IDFModel Load(string path) + { + return WrapAsIDFModel( + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_className, "load", path)); + } + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static IDFModel WrapAsIDFModel(object obj) => + new IDFModel((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/Model.cs b/src/spark/Flowthru.Spark/ML/Feature/Model.cs new file mode 100644 index 00000000..ba73cfdd --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/Model.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// A helper interface for JavaModel, so that when we have an array of JavaModels + /// with different type params, we can hold all of them with Model<object>. + /// + public interface IModel + { + bool HasParent(); + } + + /// + /// A fitted model, i.e., a Transformer produced by an Estimator. + /// + /// + /// Model Type. + /// + public abstract class JavaModel : JavaTransformer, IModel where M : JavaModel + { + internal JavaModel(string className) : base(className) + { + } + + internal JavaModel(string className, string uid) : base(className, uid) + { + } + + internal JavaModel(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Sets the parent of this model. + /// + /// The parent of the JavaModel to be set + /// type parameter M + public M SetParent(JavaEstimator parent) => + WrapAsType((JvmObjectReference)Reference.Invoke("setParent", parent)); + + /// + /// Indicates whether this Model has a corresponding parent. + /// + /// bool + public bool HasParent() => + (bool)Reference.Invoke("hasParent"); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/NGram.cs b/src/spark/Flowthru.Spark/ML/Feature/NGram.cs new file mode 100644 index 00000000..2ac28ba9 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/NGram.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// Class transformer that converts the input array of strings into + /// an array of n-grams. Null values in the input array are ignored. It returns an array + /// of n-grams where each n-gram is represented by a space-separated string of words. + /// + public class NGram : + JavaTransformer, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.NGram"; + + /// + /// Create a without any parameters. + /// + public NGram() : base(s_className) + { + } + + /// + /// Create a with a UID that is used to give the + /// a unique ID. + /// + /// An immutable unique ID for the object and its derivatives. + /// + public NGram(string uid) : base(s_className, uid) + { + } + + internal NGram(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Gets the column that the should read from. + /// + /// string, input column + public string GetInputCol() => (string)Reference.Invoke("getInputCol"); + + /// + /// Sets the column that the should read from. + /// + /// The name of the column to as the source + /// New object + public NGram SetInputCol(string value) => WrapAsNGram(Reference.Invoke("setInputCol", value)); + + /// + /// Gets the output column that the writes. + /// + /// string, the output column + public string GetOutputCol() => (string)Reference.Invoke("getOutputCol"); + + /// + /// Sets the output column that the writes. + /// + /// The name of the new column + /// New object + public NGram SetOutputCol(string value) => WrapAsNGram(Reference.Invoke("setOutputCol", value)); + + /// + /// Gets N value for . + /// + /// int, N value + public int GetN() => (int)Reference.Invoke("getN"); + + /// + /// Sets N value for . + /// + /// N value + /// New object + public NGram SetN(int value) => WrapAsNGram(Reference.Invoke("setN", value)); + + /// + /// Executes the and transforms the DataFrame to include the new + /// column. + /// + /// The DataFrame to transform + /// + /// New object with the source transformed. + /// + public override DataFrame Transform(DataFrame source) => + new DataFrame((JvmObjectReference)Reference.Invoke("transform", source)); + + /// + /// Check transform validity and derive the output schema from the input schema. + /// + /// This checks for validity of interactions between parameters during Transform and + /// raises an exception if any parameter value is invalid. + /// + /// Typical implementation should first conduct verification on schema change and parameter + /// validity, including complex parameter interaction checks. + /// + /// + /// The of the which will be transformed. + /// + /// + /// The of the output schema that would have been derived from the + /// input schema, if Transform had been called. + /// + public override StructType TransformSchema(StructType value) => + new StructType( + (JvmObjectReference)Reference.Invoke( + "transformSchema", + DataType.FromJson(Reference.Jvm, value.Json))); + + /// + /// Loads the that was previously saved using Save. + /// + /// The path the previous was saved to + /// New object, loaded from path + public static NGram Load(string path) => + WrapAsNGram( + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_className, + "load", + path)); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static NGram WrapAsNGram(object obj) => new NGram((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/Pipeline.cs b/src/spark/Flowthru.Spark/ML/Feature/Pipeline.cs new file mode 100644 index 00000000..88174524 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/Pipeline.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Utils; +using System.Collections.Generic; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// A simple pipeline, which acts as an estimator. + /// A Pipeline consists of a sequence of stages, each of which is either an Estimator or a Transformer. + /// When Pipeline.fit is called, the stages are executed in order. If a stage is an Estimator, its + /// Estimator.fit method will be called on the input dataset to fit a model. Then the model, which is a + /// transformer, will be used to transform the dataset as the input to the next stage. + /// If a stage is a Transformer, its Transformer.transform method will be called to produce the + /// dataset for the next stage. The fitted model from a Pipeline is a PipelineModel, which consists of + /// fitted models and transformers, corresponding to the pipeline + /// stages. If there are no stages, the pipeline acts as an identity transformer. + /// + public class Pipeline : + JavaEstimator, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = "org.apache.spark.ml.Pipeline"; + + /// + /// Creates a without any parameters. + /// + public Pipeline() : base(s_className) + { + } + + /// + /// Creates a with a UID that is used to give the + /// a unique ID. + /// + /// An immutable unique ID for the object and its derivatives. + public Pipeline(string uid) : base(s_className, uid) + { + } + + internal Pipeline(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Set the stages of pipeline instance. + /// + /// + /// A sequence of stages, each of which is either an Estimator or a Transformer. + /// + /// object + public Pipeline SetStages(JavaPipelineStage[] value) => + WrapAsPipeline((JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + "org.apache.spark.mllib.api.dotnet.MLUtils", + "setPipelineStages", + Reference, + value.ToJavaArrayList())); + + /// + /// Get the stages of pipeline instance. + /// + /// A sequence of stages + public JavaPipelineStage[] GetStages() + { + var jvmObjects = (JvmObjectReference[])Reference.Invoke("getStages"); + var result = new JavaPipelineStage[jvmObjects.Length]; + Dictionary classMapping = JvmObjectUtils.ConstructJavaClassMapping( + typeof(JavaPipelineStage), + "s_className"); + + for (int i = 0; i < jvmObjects.Length; i++) + { + if (JvmObjectUtils.TryConstructInstanceFromJvmObject( + jvmObjects[i], + classMapping, + out JavaPipelineStage instance)) + { + result[i] = instance; + } + } + + return result; + } + + /// Fits a model to the input data. + /// The to fit the model to. + /// + override public PipelineModel Fit(DataFrame dataset) => + new PipelineModel( + (JvmObjectReference)Reference.Invoke("fit", dataset)); + + /// + /// Loads the that was previously saved using Save(string). + /// + /// The path the previous was saved to + /// New object, loaded from path. + public static Pipeline Load(string path) => WrapAsPipeline( + SparkEnvironment.JvmBridge.CallStaticJavaMethod(s_className, "load", path)); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static Pipeline WrapAsPipeline(object obj) => + new Pipeline((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/PipelineModel.cs b/src/spark/Flowthru.Spark/ML/Feature/PipelineModel.cs new file mode 100644 index 00000000..baf3ea9c --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/PipelineModel.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// Represents a fitted pipeline. + /// + public class PipelineModel : + JavaModel, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = "org.apache.spark.ml.PipelineModel"; + + /// + /// Creates a with a UID that is used to give the + /// a unique ID, and an array of transformers as stages. + /// + /// An immutable unique ID for the object and its derivatives. + /// Stages for the PipelineModel. + public PipelineModel(string uid, JavaTransformer[] stages) + : this(SparkEnvironment.JvmBridge.CallConstructor( + s_className, uid, stages.ToJavaArrayList())) + { + } + + internal PipelineModel(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Loads the that was previously saved using Save(string). + /// + /// The path the previous was saved to + /// New object, loaded from path. + public static PipelineModel Load(string path) => WrapAsPipelineModel( + SparkEnvironment.JvmBridge.CallStaticJavaMethod(s_className, "load", path)); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static PipelineModel WrapAsPipelineModel(object obj) => + new PipelineModel((JvmObjectReference)obj); + + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/PipelineStage.cs b/src/spark/Flowthru.Spark/ML/Feature/PipelineStage.cs new file mode 100644 index 00000000..4efa9d48 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/PipelineStage.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// A stage in a pipeline, either an Estimator or a Transformer. + /// + public abstract class JavaPipelineStage : Params + { + internal JavaPipelineStage(string className) : base(className) + { + } + + internal JavaPipelineStage(string className, string uid) : base(className, uid) + { + } + + internal JavaPipelineStage(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Check transform validity and derive the output schema from the input schema. + /// + /// We check validity for interactions between parameters during transformSchema + /// and raise an exception if any parameter value is invalid. + /// + /// Typical implementation should first conduct verification on schema change and + /// parameter validity, including complex parameter interaction checks. + /// + /// + /// The of the which will be transformed. + /// + /// + /// The of the output schema that would have been derived from the + /// input schema, if Transform had been called. + /// + public virtual StructType TransformSchema(StructType schema) => + new StructType( + (JvmObjectReference)Reference.Invoke( + "transformSchema", + DataType.FromJson(Reference.Jvm, schema.Json))); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/SQLTransformer.cs b/src/spark/Flowthru.Spark/ML/Feature/SQLTransformer.cs new file mode 100644 index 00000000..9f2a5b4f --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/SQLTransformer.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// implements the transformations which are defined by SQL statement. + /// + public class SQLTransformer : + JavaTransformer, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.SQLTransformer"; + + /// + /// Create a without any parameters. + /// + public SQLTransformer() : base(s_className) + { + } + + /// + /// Create a with a UID that is used to give the + /// a unique ID. + /// + /// An immutable unique ID for the object and its derivatives. + public SQLTransformer(string uid) : base(s_className, uid) + { + } + + internal SQLTransformer(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Executes the and transforms the DataFrame to include the new + /// column. + /// + /// The DataFrame to transform + /// + /// New object with the source transformed. + /// + public override DataFrame Transform(DataFrame source) => + new DataFrame((JvmObjectReference)Reference.Invoke("transform", source)); + + /// + /// Executes the and transforms the schema. + /// + /// The Schema to be transformed + /// + /// New object with the schema transformed. + /// + public override StructType TransformSchema(StructType value) => + new StructType( + (JvmObjectReference)Reference.Invoke( + "transformSchema", + DataType.FromJson(Reference.Jvm, value.Json))); + + /// + /// Gets the statement. + /// + /// Statement + public string GetStatement() => (string)Reference.Invoke("getStatement"); + + /// + /// Sets the statement to . + /// + /// SQL Statement + /// + /// with the statement set. + /// + public SQLTransformer SetStatement(string statement) => + WrapAsSQLTransformer((JvmObjectReference)Reference.Invoke("setStatement", statement)); + + /// + /// Loads the that was previously saved using Save. + /// + /// The path the previous was saved to + /// New object, loaded from path + public static SQLTransformer Load(string path) => + WrapAsSQLTransformer( + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_className, + "load", + path)); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static SQLTransformer WrapAsSQLTransformer(object obj) => + new SQLTransformer((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/StopWordsRemover.cs b/src/spark/Flowthru.Spark/ML/Feature/StopWordsRemover.cs new file mode 100644 index 00000000..99f05a76 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/StopWordsRemover.cs @@ -0,0 +1,198 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// A feature transformer that filters out stop words from input. + /// + public class StopWordsRemover : + JavaTransformer, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.StopWordsRemover"; + + /// + /// Create a without any parameters. + /// + public StopWordsRemover() : base(s_className) + { + } + + /// + /// Create a with a UID that is used to give the + /// a unique ID. + /// + /// An immutable unique ID for the object and its derivatives. + public StopWordsRemover(string uid) : base(s_className, uid) + { + } + + internal StopWordsRemover(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Sets the column that the should read from. + /// + /// The name of the column to use as the source + /// New object + public StopWordsRemover SetInputCol(string value) => + WrapAsStopWordsRemover(Reference.Invoke("setInputCol", value)); + + /// + /// The will create a new column in the DataFrame, this is the + /// name of the new column. + /// + /// The name of the column to use as the target + /// New object + public StopWordsRemover SetOutputCol(string value) => + WrapAsStopWordsRemover(Reference.Invoke("setOutputCol", value)); + + /// + /// Executes the and transforms the DataFrame to include the new + /// column. + /// + /// The DataFrame to transform + /// + /// New object with the source transformed + /// + public override DataFrame Transform(DataFrame source) => + new DataFrame((JvmObjectReference)Reference.Invoke("transform", source)); + + /// + /// Gets the column that the should read from. + /// + /// Input column name + public string GetInputCol() => (string)Reference.Invoke("getInputCol"); + + /// + /// The will create a new column in the DataFrame, this is the + /// name of the new column. + /// + /// The output column name + public string GetOutputCol() => (string)Reference.Invoke("getOutputCol"); + + /// + /// Sets locale for transform. + /// Refer java.util.locale.getavailablelocales() for all available locales. + /// + /// Locale to be used for transform + /// New object + [Since(Versions.V2_4_0)] + public StopWordsRemover SetLocale(string value) => + WrapAsStopWordsRemover(Reference.Invoke("setLocale", value)); + + /// + /// Gets locale for transform + /// + /// The locale + [Since(Versions.V2_4_0)] + public string GetLocale() => (string)Reference.Invoke("getLocale"); + + /// + /// Sets case sensitivity. + /// + /// true if case sensitive, false otherwise + /// New object + public StopWordsRemover SetCaseSensitive(bool value) => + WrapAsStopWordsRemover(Reference.Invoke("setCaseSensitive", value)); + + /// + /// Gets case sensitivity. + /// + /// true if case sensitive, false otherwise + public bool GetCaseSensitive() => (bool)Reference.Invoke("getCaseSensitive"); + + /// + /// Sets custom stop words. + /// + /// Custom stop words + /// New object + public StopWordsRemover SetStopWords(IEnumerable values) => + WrapAsStopWordsRemover(Reference.Invoke("setStopWords", values)); + + /// + /// Gets the custom stop words. + /// + /// Custom stop words + public IEnumerable GetStopWords() => + (IEnumerable)Reference.Invoke("getStopWords"); + + /// + /// Check transform validity and derive the output schema from the input schema. + /// + /// This checks for validity of interactions between parameters during Transform and + /// raises an exception if any parameter value is invalid. + /// + /// Typical implementation should first conduct verification on schema change and parameter + /// validity, including complex parameter interaction checks. + /// + /// + /// The of the which will be transformed. + /// + /// + /// The of the output schema that would have been derived from the + /// input schema, if Transform had been called. + /// + public override StructType TransformSchema(StructType value) => + new StructType( + (JvmObjectReference)Reference.Invoke( + "transformSchema", + DataType.FromJson(Reference.Jvm, value.Json))); + + /// + /// Load default stop words of given language for + /// transform. + /// Supported languages: danish, dutch, english, finnish, french, german, + /// hungarian, italian, norwegian, portuguese, russian, spanish, swedish, turkish. + /// + /// Language + /// Default stop words for the given language + public static string[] LoadDefaultStopWords(string language) => + (string[])SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_className, "loadDefaultStopWords", language); + + /// + /// Loads the that was previously saved using Save. + /// + /// The path the previous was saved to + /// New object, loaded from path + public static StopWordsRemover Load(string path) => + WrapAsStopWordsRemover( + SparkEnvironment.JvmBridge.CallStaticJavaMethod(s_className, "load", path)); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static StopWordsRemover WrapAsStopWordsRemover(object obj) => + new StopWordsRemover((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/Tokenizer.cs b/src/spark/Flowthru.Spark/ML/Feature/Tokenizer.cs new file mode 100644 index 00000000..d3c64d82 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/Tokenizer.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// A that converts the input string to lowercase and then splits it by + /// white spaces. + /// + public class Tokenizer : + JavaTransformer, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.Tokenizer"; + + /// + /// Create a without any parameters + /// + public Tokenizer() : base(s_className) + { + } + + /// + /// Create a with a UID that is used to give the + /// a unique ID + /// + /// An immutable unique ID for the object and its derivatives. + public Tokenizer(string uid) : base(s_className, uid) + { + } + + internal Tokenizer(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Gets the column that the should read from + /// + /// string, input column + public string GetInputCol() => (string)(Reference.Invoke("getInputCol")); + + /// + /// Sets the column that the should read from + /// + /// The name of the column to as the source + /// New object + public Tokenizer SetInputCol(string value) => + WrapAsTokenizer(Reference.Invoke("setInputCol", value)); + + /// + /// The will create a new column in the DataFrame, this is the + /// name of the new column. + /// + /// string, the output column + public string GetOutputCol() => (string)(Reference.Invoke("getOutputCol")); + + /// + /// The will create a new column in the DataFrame, this is the + /// name of the new column. + /// + /// The name of the new column + /// New object + public Tokenizer SetOutputCol(string value) => + WrapAsTokenizer(Reference.Invoke("setOutputCol", value)); + + /// + /// Executes the and transforms the DataFrame to include the new + /// column + /// + /// The DataFrame to transform + /// + /// New object with the source transformed + /// + public override DataFrame Transform(DataFrame source) => + new DataFrame((JvmObjectReference)Reference.Invoke("transform", source)); + + /// + /// Loads the that was previously saved using Save + /// + /// The path the previous was saved to + /// New object, loaded from path + public static Tokenizer Load(string path) + { + return WrapAsTokenizer( + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_className, "load", path)); + } + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static Tokenizer WrapAsTokenizer(object obj) => + new Tokenizer((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/Transformer.cs b/src/spark/Flowthru.Spark/ML/Feature/Transformer.cs new file mode 100644 index 00000000..ab9c2068 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/Transformer.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Sql; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// Abstract class for transformers that transform one dataset into another. + /// + public abstract class JavaTransformer : JavaPipelineStage + { + internal JavaTransformer(string className) : base(className) + { + } + + internal JavaTransformer(string className, string uid) : base(className, uid) + { + } + + internal JavaTransformer(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Executes the transformer and transforms the DataFrame to include new columns. + /// + /// The Dataframe to be transformed. + /// + /// containing the original data and new columns. + /// + public virtual DataFrame Transform(DataFrame dataset) => + new DataFrame((JvmObjectReference)Reference.Invoke("transform", dataset)); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/Word2Vec.cs b/src/spark/Flowthru.Spark/ML/Feature/Word2Vec.cs new file mode 100644 index 00000000..24e4e055 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/Word2Vec.cs @@ -0,0 +1,218 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.ML.Feature +{ + public class Word2Vec : + JavaEstimator, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.Word2Vec"; + + /// + /// Create a without any parameters + /// + public Word2Vec() : base(s_className) + { + } + + /// + /// Create a with a UID that is used to give the + /// a unique ID + /// + /// An immutable unique ID for the object and its derivatives. + public Word2Vec(string uid) : base(s_className, uid) + { + } + + internal Word2Vec(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Gets the column that the should read from. + /// + /// The name of the input column. + public string GetInputCol() => (string)(Reference.Invoke("getInputCol")); + + /// + /// Sets the column that the should read from. + /// + /// The name of the column to as the source. + /// + public Word2Vec SetInputCol(string value) => + WrapAsWord2Vec(Reference.Invoke("setInputCol", value)); + + /// + /// The will create a new column in the DataFrame, this is the + /// name of the new column. + /// + /// The name of the output column. + public string GetOutputCol() => (string)(Reference.Invoke("getOutputCol")); + + /// + /// The will create a new column in the DataFrame, this is the + /// name of the new column. + /// + /// The name of the output column which will be created. + /// New + public Word2Vec SetOutputCol(string value) => + WrapAsWord2Vec(Reference.Invoke("setOutputCol", value)); + + /// + /// Gets the vector size, the dimension of the code that you want to transform from words. + /// + /// + /// The vector size, the dimension of the code that you want to transform from words. + /// + public int GetVectorSize() => (int)(Reference.Invoke("getVectorSize")); + + /// + /// Sets the vector size, the dimension of the code that you want to transform from words. + /// + /// + /// The dimension of the code that you want to transform from words. + /// + /// + public Word2Vec SetVectorSize(int value) => + WrapAsWord2Vec(Reference.Invoke("setVectorSize", value)); + + /// + /// Gets the minimum number of times a token must appear to be included in the word2vec + /// model's vocabulary. + /// + /// + /// The minimum number of times a token must appear to be included in the word2vec model's + /// vocabulary. + /// + public int GetMinCount() => (int)Reference.Invoke("getMinCount"); + + /// + /// The minimum number of times a token must appear to be included in the word2vec model's + /// vocabulary. + /// + /// + /// The minimum number of times a token must appear to be included in the word2vec model's + /// vocabulary, the default is 5. + /// + /// + public virtual Word2Vec SetMinCount(int value) => + WrapAsWord2Vec(Reference.Invoke("setMinCount", value)); + + /// Gets the maximum number of iterations. + /// The maximum number of iterations. + public int GetMaxIter() => (int)Reference.Invoke("getMaxIter"); + + /// Maximum number of iterations (>= 0). + /// The number of iterations. + /// + public Word2Vec SetMaxIter(int value) => + WrapAsWord2Vec(Reference.Invoke("setMaxIter", value)); + + /// + /// Gets the maximum length (in words) of each sentence in the input data. + /// + /// The maximum length (in words) of each sentence in the input data. + public virtual int GetMaxSentenceLength() => + (int)Reference.Invoke("getMaxSentenceLength"); + + /// + /// Sets the maximum length (in words) of each sentence in the input data. + /// + /// + /// The maximum length (in words) of each sentence in the input data. + /// + /// + public Word2Vec SetMaxSentenceLength(int value) => + WrapAsWord2Vec(Reference.Invoke("setMaxSentenceLength", value)); + + /// Gets the number of partitions for sentences of words. + /// The number of partitions for sentences of words. + public int GetNumPartitions() => (int)Reference.Invoke("getNumPartitions"); + + /// Sets the number of partitions for sentences of words. + /// + /// The number of partitions for sentences of words, default is 1. + /// + /// + public Word2Vec SetNumPartitions(int value) => + WrapAsWord2Vec(Reference.Invoke("setNumPartitions", value)); + + /// Gets the value that is used for the random seed. + /// The value that is used for the random seed. + public long GetSeed() => (long)Reference.Invoke("getSeed"); + + /// Random seed. + /// The value to use for the random seed. + /// + public Word2Vec SetSeed(long value) => + WrapAsWord2Vec(Reference.Invoke("setSeed", value)); + + /// Gets the size to be used for each iteration of optimization. + /// The size to be used for each iteration of optimization. + public double GetStepSize() => (double)Reference.Invoke("getStepSize"); + + /// Step size to be used for each iteration of optimization (> 0). + /// Value to use for the step size. + /// + public Word2Vec SetStepSize(double value) => + WrapAsWord2Vec(Reference.Invoke("setStepSize", value)); + + /// Gets the window size (context words from [-window, window]). + /// The window size. + public int GetWindowSize() => (int)Reference.Invoke("getWindowSize"); + + /// The window size (context words from [-window, window]). + /// + /// The window size (context words from [-window, window]), default is 5. + /// + /// + public Word2Vec SetWindowSize(int value) => + WrapAsWord2Vec(Reference.Invoke("setWindowSize", value)); + + /// Fits a model to the input data. + /// The to fit the model to. + /// + public override Word2VecModel Fit(DataFrame dataFrame) => + new Word2VecModel((JvmObjectReference)Reference.Invoke("fit", dataFrame)); + + /// + /// Loads the that was previously saved using Save(string). + /// + /// The path the previous was saved to + /// New object, loaded from path. + public static Word2Vec Load(string path) => WrapAsWord2Vec( + SparkEnvironment.JvmBridge.CallStaticJavaMethod(s_className, "load", path)); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static Word2Vec WrapAsWord2Vec(object obj) => + new Word2Vec((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Feature/Word2VecModel.cs b/src/spark/Flowthru.Spark/ML/Feature/Word2VecModel.cs new file mode 100644 index 00000000..b8c4296b --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Feature/Word2VecModel.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.ML.Feature +{ + public class Word2VecModel : + JavaModel, + IJavaMLWritable, + IJavaMLReadable + { + private static readonly string s_className = + "org.apache.spark.ml.feature.Word2VecModel"; + + /// + /// Create a without any parameters + /// + public Word2VecModel() : base(s_className) + { + } + + /// + /// Create a with a UID that is used to give the + /// a unique ID + /// + /// An immutable unique ID for the object and its derivatives. + public Word2VecModel(string uid) : base(s_className, uid) + { + } + + internal Word2VecModel(JvmObjectReference jvmObject) : base(jvmObject) + { + } + + /// + /// Transform a sentence column to a vector column to represent the whole sentence. + /// + /// to transform + public override DataFrame Transform(DataFrame documentDF) => + new DataFrame((JvmObjectReference)Reference.Invoke("transform", documentDF)); + + /// + /// Find number of words whose vector representation most similar to + /// the supplied vector. If the supplied vector is the vector representation of a word in + /// the model's vocabulary, that word will be in the results. Returns a dataframe with the + /// words and the cosine similarities between the synonyms and the given word vector. + /// + /// The "word" to find similarities for, this can be a string or a + /// vector representation. + /// The number of words to find that are similar to "word" + public DataFrame FindSynonyms(string word, int num) => + new DataFrame((JvmObjectReference)Reference.Invoke("findSynonyms", word, num)); + + /// + /// Loads the that was previously saved using Save(string). + /// + /// + /// The path the previous was saved to + /// + /// New object, loaded from path. + public static Word2VecModel Load(string path) => WrapAsWord2VecModel( + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_className, "load", path)); + + /// + /// Saves the object so that it can be loaded later using Load. Note that these objects + /// can be shared with Scala by Loading or Saving in Scala. + /// + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + public JavaMLWriter Write() => + new JavaMLWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + public JavaMLReader Read() => + new JavaMLReader((JvmObjectReference)Reference.Invoke("read")); + + private static Word2VecModel WrapAsWord2VecModel(object obj) => + new Word2VecModel((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Param/Param.cs b/src/spark/Flowthru.Spark/ML/Param/Param.cs new file mode 100644 index 00000000..ece0d120 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Param/Param.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.ML.Feature.Param +{ + /// + /// A with self-contained documentation and optionally default value. + /// + /// A references an individual parameter that includes documentation, the + /// name of the parameter and optionally a default value. Params can either be set using the + /// generic methods or by using explicit methods. For example + /// has SetHandleInvalid or you can call + /// GetParam("handleInvalid")and then . Set using the + /// and the value you want to use. + /// + public class Param : IJvmObjectReferenceProvider + { + private static readonly string s_ParamClassName = + "org.apache.spark.ml.param.Param"; + + /// + /// Creates a new instance of a which will be attached to the parent + /// specified. The most likely use case for a is being read from a + /// parent object such as rather than independently + /// The parent object to assign the to + /// The name of this + /// The documentation for this + /// + public Param(Identifiable parent, string name, string doc) + : this(SparkEnvironment.JvmBridge.CallConstructor( + s_ParamClassName, parent.Uid(), name, doc)) + { + } + + /// + /// Creates a new instance of a which will be attached to the parent + /// with the UID specified. The most likely use case for a is being + /// read from a parent object such as rather than independently + /// + /// The UID of the parent object to assign the to + /// + /// The name of this + /// The documentation for this + /// + public Param(string parent, string name, string doc) + : this(SparkEnvironment.JvmBridge.CallConstructor(s_ParamClassName, parent, name, doc)) + { + } + + internal Param(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + + /// + /// The description of what the does and how it works including any + /// defaults and the current value + /// + /// A description of how the works + public string Doc => (string)Reference.Invoke("doc"); + + /// + /// The name of the + /// + /// The name of the + public string Name => (string)Reference.Invoke("name"); + + /// + /// The object that contains the + /// + /// The UID of the parent oject that this belongs to + public string Parent => (string)Reference.Invoke("parent"); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Param/ParamMap.cs b/src/spark/Flowthru.Spark/ML/Param/ParamMap.cs new file mode 100644 index 00000000..e62594ee --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Param/ParamMap.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.ML.Feature.Param +{ + /// + /// A param to value map. + /// + public class ParamMap : IJvmObjectReferenceProvider + { + private static readonly string s_ParamMapClassName = "org.apache.spark.ml.param.ParamMap"; + + /// + /// Creates a new instance of a + /// + public ParamMap() : this(SparkEnvironment.JvmBridge.CallConstructor(s_ParamMapClassName)) + { + } + + internal ParamMap(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + + /// + /// Puts a (param, value) pair (overwrites if the input param exists). + /// + /// The param to be add + /// The param value to be add + public ParamMap Put(Param param, T value) => + WrapAsParamMap((JvmObjectReference)Reference.Invoke("put", param, value)); + + /// + /// Returns the string representation of this ParamMap. + /// + /// representation as string value. + public override string ToString() => + (string)Reference.Invoke("toString"); + + private static ParamMap WrapAsParamMap(object obj) => + new ParamMap((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Param/ParamPair.cs b/src/spark/Flowthru.Spark/ML/Param/ParamPair.cs new file mode 100644 index 00000000..f26b75a5 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Param/ParamPair.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.ML.Feature.Param +{ + /// + /// A param and its value. + /// + public sealed class ParamPair : IJvmObjectReferenceProvider + { + private static readonly string s_ParamPairClassName = "org.apache.spark.ml.param.ParamPair"; + + /// + /// Creates a new instance of a + /// + public ParamPair(Param param, T value) + : this(SparkEnvironment.JvmBridge.CallConstructor(s_ParamPairClassName, param, value)) + { + } + + internal ParamPair(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + } +} diff --git a/src/spark/Flowthru.Spark/ML/Util/Identifiable.cs b/src/spark/Flowthru.Spark/ML/Util/Identifiable.cs new file mode 100644 index 00000000..6b0be1e6 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Util/Identifiable.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Flowthru.Spark.ML.Feature +{ + public interface Identifiable + { + /// + /// The UID of the object. + /// + /// string UID identifying the object + string Uid(); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Util/Read.cs b/src/spark/Flowthru.Spark/ML/Util/Read.cs new file mode 100644 index 00000000..cbf74d7d --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Util/Read.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Reflection; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// Class for utility classes that can load ML instances. + /// + /// ML instance type + public class JavaMLReader : IJvmObjectReferenceProvider + { + internal JavaMLReader(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + + /// + /// Loads the ML component from the input path. + /// + /// The path the previous instance of type T was saved to + /// The type T instance + public T Load(string path) => + WrapAsType((JvmObjectReference)Reference.Invoke("load", path)); + + /// Sets the Spark Session to use for saving/loading. + /// The Spark Session to be set + public JavaMLReader Session(SparkSession sparkSession) + { + Reference.Invoke("session", sparkSession); + return this; + } + + private static T WrapAsType(JvmObjectReference reference) + { + ConstructorInfo constructor = typeof(T) + .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(c => + { + ParameterInfo[] parameters = c.GetParameters(); + return (parameters.Length == 1) && + (parameters[0].ParameterType == typeof(JvmObjectReference)); + }); + + return (T)constructor.Invoke(new object[] { reference }); + } + } + + /// + /// Interface for objects that provide MLReader. + /// + /// + /// ML instance type + /// + public interface IJavaMLReadable + { + /// + /// Get the corresponding JavaMLReader instance. + /// + /// an instance for this ML instance. + JavaMLReader Read(); + } +} diff --git a/src/spark/Flowthru.Spark/ML/Util/Write.cs b/src/spark/Flowthru.Spark/ML/Util/Write.cs new file mode 100644 index 00000000..765c3e44 --- /dev/null +++ b/src/spark/Flowthru.Spark/ML/Util/Write.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.ML.Feature +{ + /// + /// Class for utility classes that can save ML instances in Spark's internal format. + /// + public class JavaMLWriter : IJvmObjectReferenceProvider + { + internal JavaMLWriter(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + + /// Saves the ML instances to the input path. + /// The path to save the object to + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// save() handles overwriting and then calls this method. + /// Subclasses should override this method to implement the actual saving of the instance. + /// + /// The path to save the object to + protected void SaveImpl(string path) => Reference.Invoke("saveImpl", path); + + /// Overwrites if the output path already exists. + public JavaMLWriter Overwrite() + { + Reference.Invoke("overwrite"); + return this; + } + + /// + /// Adds an option to the underlying MLWriter. See the documentation for the specific model's + /// writer for possible options. The option name (key) is case-insensitive. + /// + /// key of the option + /// value of the option + public JavaMLWriter Option(string key, string value) + { + Reference.Invoke("option", key, value); + return this; + } + + /// Sets the Spark Session to use for saving/loading. + /// The Spark Session to be set + public JavaMLWriter Session(SparkSession sparkSession) + { + Reference.Invoke("session", sparkSession); + return this; + } + } + + /// + /// Interface for classes that provide JavaMLWriter. + /// + public interface IJavaMLWritable + { + /// + /// Get the corresponding JavaMLWriter instance. + /// + /// a instance for this ML instance. + JavaMLWriter Write(); + + /// Saves this ML instance to the input path + /// The path to save the object to + void Save(string path); + } +} diff --git a/src/spark/Flowthru.Spark/Network/DefaultSocketWrapper.cs b/src/spark/Flowthru.Spark/Network/DefaultSocketWrapper.cs new file mode 100644 index 00000000..20b6f765 --- /dev/null +++ b/src/spark/Flowthru.Spark/Network/DefaultSocketWrapper.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using Flowthru.Spark.Services; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Network +{ + /// + /// A simple wrapper of System.Net.Sockets.Socket class. + /// + internal sealed class DefaultSocketWrapper : ISocketWrapper + { + private readonly Socket _innerSocket; + private Stream _inputStream; + private Stream _outputStream; + + /// + /// Default constructor that creates a new instance of DefaultSocket class which represents + /// a traditional socket (System.Net.Socket.Socket). + /// + /// This socket is bound to Loopback with port 0. + /// + public DefaultSocketWrapper() : + this(new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + _innerSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + } + + /// + /// Initializes a instance of DefaultSocket class using the specified + /// System.Net.Socket.Socket object. + /// + /// The existing socket + private DefaultSocketWrapper(Socket socket) + { + // Disable Nagle algorithm, which works to combine small packets together at the + // expense of responsiveness; it's focused on reducing congestion on slow networks, + // but all of our accesses are on localhost. + socket.NoDelay = true; + + _innerSocket = socket; + } + + /// + /// Releases all resources used by the current instance of the DefaultSocket class. + /// + public void Dispose() + { + _outputStream?.Dispose(); + _inputStream?.Dispose(); + _innerSocket.Dispose(); + } + + /// + /// Accepts a incoming connection request. + /// + /// A DefaultSocket instance used to send and receive data + public ISocketWrapper Accept() => new DefaultSocketWrapper(_innerSocket.Accept()); + + /// + /// Establishes a connection to a remote host that is specified by an IP address and + /// a port number. + /// + /// The IP address of the remote host + /// The port number of the remote host + /// Secret string to use for connection + public void Connect(IPAddress remoteaddr, int port, string secret) + { + _innerSocket.Connect(new IPEndPoint(remoteaddr, port)); + + if (!string.IsNullOrWhiteSpace(secret)) + { + using NetworkStream stream = CreateNetworkStream(); + if (!Authenticator.AuthenticateAsClient(stream, secret)) + { + throw new Exception($"Failed to authenticate for port: {port}."); + } + } + } + + /// + /// Returns the NetworkStream used to send and receive data. + /// + /// + /// GetStream returns a NetworkStream that you can use to send and receive data. + /// You must close/dispose the NetworkStream by yourself. Closing DefaultSocketWrapper + /// does not release the NetworkStream. + /// + /// The underlying Stream instance that be used to send and receive data + private NetworkStream CreateNetworkStream() => new NetworkStream(_innerSocket); + + /// + /// Returns a stream used to receive data only. + /// + /// The underlying Stream instance that be used to receive data + public Stream InputStream => + _inputStream ??= CreateStream( + ConfigurationService.WorkerReadBufferSizeEnvVarName); + + /// + /// Returns a stream used to send data only. + /// + /// The underlying Stream instance that be used to send data + public Stream OutputStream => + _outputStream ??= CreateStream( + ConfigurationService.WorkerWriteBufferSizeEnvVarName); + + private Stream CreateStream(string bufferSizeEnvVarName) + { + string envVar = Environment.GetEnvironmentVariable(bufferSizeEnvVarName); + if (string.IsNullOrEmpty(envVar) || + !int.TryParse(envVar, out var writeBufferSize)) + { + // The default buffer size is 64K, PythonRDD also use 64K as default buffer size. + writeBufferSize = 64 * 1024; + } + + Stream ns = CreateNetworkStream(); + return (writeBufferSize > 0) ? new BufferedStream(ns, writeBufferSize) : ns; + } + + /// + /// Starts listening for incoming connections requests + /// + /// The maximum length of the pending connections queue. + public void Listen(int backlog = 16) => _innerSocket.Listen(backlog); + + /// + /// Returns the local endpoint. + /// + public EndPoint LocalEndPoint => _innerSocket.LocalEndPoint; + + /// + /// Returns the remote endpoint. + /// + public EndPoint RemoteEndPoint => _innerSocket.RemoteEndPoint; + } +} diff --git a/src/spark/Flowthru.Spark/Network/ISocketWrapper.cs b/src/spark/Flowthru.Spark/Network/ISocketWrapper.cs new file mode 100644 index 00000000..31e67162 --- /dev/null +++ b/src/spark/Flowthru.Spark/Network/ISocketWrapper.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Net; + +namespace Flowthru.Spark.Network +{ + /// + /// ISocketWrapper interface defines the common methods to operate a socket. + /// + internal interface ISocketWrapper : IDisposable + { + /// + /// Accepts a incoming connection request. + /// + /// A ISocket instance used to send and receive data + ISocketWrapper Accept(); + + /// + /// Establishes a connection to a remote host that is specified by an IP address and + /// a port number. + /// + /// The IP address of the remote host + /// The port number of the remote host + /// Optional secret string to use for connection + void Connect(IPAddress remoteaddr, int port, string secret = null); + + /// + /// Returns a stream used to receive data only. + /// + /// The underlying Stream instance that be used to receive data + Stream InputStream { get; } + + /// + /// Returns a stream used to send data only. + /// + /// The underlying Stream instance that be used to send data + Stream OutputStream { get; } + + /// + /// Starts listening for incoming connections requests + /// + /// The maximum length of the pending connections queue + void Listen(int backlog = 16); + + /// + /// Returns the local endpoint. + /// + EndPoint LocalEndPoint { get; } + + /// + /// Returns the remote endpoint. + /// + EndPoint RemoteEndPoint { get; } + } +} diff --git a/src/spark/Flowthru.Spark/Network/SocketFactory.cs b/src/spark/Flowthru.Spark/Network/SocketFactory.cs new file mode 100644 index 00000000..f5213ffe --- /dev/null +++ b/src/spark/Flowthru.Spark/Network/SocketFactory.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Flowthru.Spark.Network +{ + /// + /// SocketFactory is used to create ISocketWrapper instance. + /// + internal static class SocketFactory + { + /// + /// Creates an ISocket instance based on the socket type set. + /// + /// + /// ISocketWrapper instance. + /// + public static ISocketWrapper CreateSocket() + { + return new DefaultSocketWrapper(); + } + } +} diff --git a/src/spark/Flowthru.Spark/PairRDDFunctions.cs b/src/spark/Flowthru.Spark/PairRDDFunctions.cs new file mode 100644 index 00000000..54a7026f --- /dev/null +++ b/src/spark/Flowthru.Spark/PairRDDFunctions.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Flowthru.Spark +{ + /// + /// Extra functions available on RDDs of (key, value) pairs through extension methods. + /// + internal static class PairRDDFunctions + { + /// + /// Returns the key-value pairs in this RDD as a dictionary. + /// + /// Type of the key + /// Type of the value + /// RDD object to apply + /// Dictionary of RDD content + public static IDictionary CollectAsMap( + this RDD> self) + { + return self.Collect().ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2); + } + + /// + /// Return an RDD with the keys of each tuple. + /// + /// Type of the key + /// Type of the value + /// RDD object to apply + /// RDD with the keys of each tuple + public static RDD Keys(this RDD> self) + { + return self.Map(tuple => tuple.Item1); + } + + /// + /// Return an RDD with the values of each tuple. + /// + /// Type of the key + /// Type of the value + /// RDD object to apply + /// RDD with the values of each tuple + public static RDD Values(this RDD> self) + { + return self.Map(tuple => tuple.Item2); + } + } +} diff --git a/src/spark/Flowthru.Spark/RDD.cs b/src/spark/Flowthru.Spark/RDD.cs new file mode 100644 index 00000000..4686b4b2 --- /dev/null +++ b/src/spark/Flowthru.Spark/RDD.cs @@ -0,0 +1,528 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Network; +using Flowthru.Spark.Utils; +using static Flowthru.Spark.Utils.CommandSerDe; + +namespace Flowthru.Spark +{ + /// + /// A Resilient Distributed Dataset(RDD), the basic abstraction in Spark, + /// represents an immutable, partitioned collection of elements that can be + /// operated on in parallel. This class contains the basic operations available + /// on all RDDs. + /// + /// Type of the elements in the RDD + internal class RDD : IJvmObjectReferenceProvider + { + /// + /// The JVM object for this RDD. This can be null if the current + /// RDD is not materialized. + /// + internal JvmObjectReference _jvmObject = null; + + /// + /// The previous RDD object that this RDD references to. This is + /// used by to chain RDD operations. + /// + internal readonly JvmObjectReference _prevRddJvmObjRef = null; + + /// + /// SparkContext object associated with the RDD. + /// + internal readonly SparkContext _sparkContext = null; + + /// + /// Flag that checks if is called. + /// + protected bool _isCached = false; + + /// + /// Flag that checks if is called. + /// + protected bool _isCheckpointed = false; + + /// + /// Serialization mode for the current RDD. This will be + /// translated into serialization mode while creating a serialized command. + /// + internal SerializedMode _serializedMode = SerializedMode.Byte; + + /// + /// Serialization mode for the previously pipelined RDD. This will be + /// translated into deserialization mode while creating a serialized command. + /// + internal SerializedMode _prevSerializedMode = SerializedMode.Byte; + + /// + /// Constructor mainly called by SparkContext for creating the first RDD + /// via , etc. + /// + /// The reference to the RDD JVM object + /// SparkContext object + /// Serialization mode for the current RDD + internal RDD( + JvmObjectReference jvmObject, + SparkContext sparkContext, + SerializedMode serializedMode) + { + _jvmObject = jvmObject; + _sparkContext = sparkContext; + _serializedMode = serializedMode; + } + + /// + /// Constructor mainly called by . + /// + /// + /// The reference to the RDD JVM object from which pipeline is created + /// + /// SparkContext object + /// Serialization mode for the current RDD + /// Serialization mode for the previous RDD + internal RDD( + JvmObjectReference prevRddJvmObjRef, + SparkContext sparkContext, + SerializedMode serializedMode, + SerializedMode prevSerializedMode) + { + // This constructor is called from PipelineRDD constructor + // where the _jvmObject is not yet created. + + _prevRddJvmObjRef = prevRddJvmObjRef; + _sparkContext = sparkContext; + _serializedMode = serializedMode; + _prevSerializedMode = prevSerializedMode; + } + + JvmObjectReference IJvmObjectReferenceProvider.Reference => _jvmObject; + + /// + /// Persist this RDD with the default storage level (MEMORY_ONLY). + /// + /// + public RDD Cache() + { + GetJvmRef().Invoke("cache"); + _isCached = true; + return this; + } + + /// + /// Mark this RDD for checkpointing. It will be saved to a file inside the checkpoint + /// directory set with and all + /// references to its parent RDDs will be removed. This function must be called before + /// any job has been executed on this RDD. It is strongly recommended that this RDD is + /// persisted in memory, otherwise saving it in a file will require re-computation. + /// + public void Checkpoint() + { + GetJvmRef().Invoke("checkpoint"); + _isCheckpointed = true; + } + + /// + /// Return a new RDD by applying a function to all elements of this RDD. + /// + /// Type of the new RDD elements + /// Function to apply + /// Flag to preserve partitioning + /// New RDD by applying a given function + public RDD Map(Func func, bool preservesPartitioning = false) + { + return MapPartitionsWithIndexInternal( + new MapUdfWrapper(func).Execute, + preservesPartitioning); + } + + /// + /// Return a new RDD by first applying a function to all elements of this RDD, + /// and then flattening the results. + /// + /// Type of the new RDD elements + /// Function to apply + /// Flag to preserve partitioning + /// New RDD by applying a given function + public RDD FlatMap(Func> func, bool preservesPartitioning = false) + { + return MapPartitionsWithIndexInternal( + new FlatMapUdfWrapper(func).Execute, + preservesPartitioning); + } + + /// + /// Return a new RDD by applying a function to each partition of this RDD. + /// + /// + /// + /// "preservesPartitioning" indicates whether the input function preserves the + /// partitioner, which should be false unless this is a pair RDD and the input + /// function doesn't modify the keys. + /// + /// Type of the new RDD elements + /// Function to apply + /// Flag to preserve partitioning + /// New RDD by applying a given function + public RDD MapPartitions( + Func, IEnumerable> func, + bool preservesPartitioning = false) + { + return MapPartitionsWithIndexInternal( + new MapPartitionsUdfWrapper(func).Execute, + preservesPartitioning); + } + + /// + /// Return a new RDD by applying a function to each partition of this RDD, + /// while tracking the index of the original partition. + /// + /// + /// "preservesPartitioning" indicates whether the input function preserves the + /// partitioner, which should be false unless this is a pair RDD and the input + /// function doesn't modify the keys. + /// + /// Type of the new RDD elements + /// Function to apply + /// Flag to preserve partitioning + /// New RDD by applying a given function + public RDD MapPartitionsWithIndex( + Func, IEnumerable> func, + bool preservesPartitioning = false) + { + return MapPartitionsWithIndexInternal( + new MapPartitionsWithIndexUdfWrapper(func).Execute, + preservesPartitioning); + } + + /// + /// Return the number of partitions in this RDD. + /// + /// The number of partitions in this RDD + public int GetNumPartitions() + { + return (int)GetJvmRef().Invoke("getNumPartitions"); + } + + /// + /// Return a new RDD containing only the elements that satisfy a predicate. + /// + /// Predicate function to apply + /// A new RDD with elements that satisfy a predicate + public RDD Filter(Func func) + { + return MapPartitionsWithIndexInternal(new FilterUdfWrapper(func).Execute, true); + } + + /// + /// Return a sampled subset of this RDD with a seed. + /// + /// + /// This is NOT guaranteed to provide exactly the fraction of the count + /// of the given RDD. + /// + /// True if elements be sampled multiple times + /// + /// Expected size of the sample as a fraction of this RDD's size without replacement + /// + /// Optional user-supplied seed (random seed if not provided) + /// A sampled subset of this RDD + public RDD Sample(bool withReplacement, double fraction, long? seed = null) + { + return new RDD( + (seed.HasValue) ? + (JvmObjectReference)GetJvmRef().Invoke( + "sample", + withReplacement, + fraction, + seed.GetValueOrDefault()) : + (JvmObjectReference)GetJvmRef().Invoke( + "sample", + withReplacement, + fraction), + _sparkContext, + _serializedMode); + } + + /// + /// Return an enumerable collection that contains all of the elements in this RDD. + /// + /// + /// This method should only be used if the resulting array is expected to be small, + /// as all the data is loaded into the driver's memory. + /// + /// An enumerable collection of all the elements. + public IEnumerable Collect() + { + (int port, string secret) = CollectAndServe(); + using ISocketWrapper socket = SocketFactory.CreateSocket(); + socket.Connect(IPAddress.Loopback, port, secret); + + var collector = new RDD.Collector(); + System.IO.Stream stream = socket.InputStream; + foreach (T element in collector.Collect(stream, _serializedMode).Cast()) + { + yield return element; + } + } + + /// + /// Helper function for creating PipelinedRDD. + /// + /// Type of the new RDD elements + /// Function to apply + /// Flag to preserve partitioning + /// New RDD by applying a given function + internal virtual RDD MapPartitionsWithIndexInternal( + RDD.WorkerFunction.ExecuteDelegate func, + bool preservesPartitioning = false) + { + return new PipelinedRDD( + new RDD.WorkerFunction(func), + preservesPartitioning, + _jvmObject, + _sparkContext, + _serializedMode); + } + + /// + /// Returns the socket info by calling collectAndServe on the RDD object. + /// + /// Socket info + private (int, string) CollectAndServe() + { + JvmObjectReference rddRef = GetJvmRef(); + // collectToPython() returns a pair where the first is a port number + // and the second is the secret string to use for the authentication. + var pair = (JvmObjectReference[])rddRef.Jvm.CallStaticJavaMethod( + "org.apache.spark.api.python.PythonRDD", + "collectAndServe", + rddRef.Invoke("rdd")); + return ((int)pair[0].Invoke("intValue"), (string)pair[1].Invoke("toString")); + } + + /// + /// Returns the JvmObjectReference object of this RDD. + /// + /// + /// It is possible that the JvmObjectReference object is null depending + /// on how the RDD object is instantiated (e.g., ). Thus, + /// this function should be used instead of directly accessing _jvmObject because the + /// derived class (e.g., ) can override the behavior + /// when _jvmObject is null. + /// + /// JvmObjetReference object for this RDD + private JvmObjectReference GetJvmRef() + { + return ((IJvmObjectReferenceProvider)this).Reference; + } + + /// + /// Helper to map the UDF for Map() to . + /// + /// Input type + /// Output type + [UdfWrapper] + internal sealed class MapUdfWrapper + { + private readonly Func _func; + + internal MapUdfWrapper(Func func) + { + _func = func; + } + + internal IEnumerable Execute(int _, IEnumerable input) + { + return input.Cast().Select(_func).Cast(); + } + } + + /// + /// Helper to map the UDF for FlatMap() to + /// + /// Input type + /// Output type + [UdfWrapper] + internal sealed class FlatMapUdfWrapper + { + private readonly Func> _func; + + internal FlatMapUdfWrapper(Func> func) + { + _func = func; + } + + internal IEnumerable Execute(int _, IEnumerable input) + { + return input.Cast().SelectMany(_func).Cast(); + } + } + + /// + /// Helper to map the UDF for MapPartitions() to + /// . + /// + /// Input type + /// Output type + [UdfWrapper] + internal sealed class MapPartitionsUdfWrapper + { + private readonly Func, IEnumerable> _func; + + internal MapPartitionsUdfWrapper(Func, IEnumerable> func) + { + _func = func; + } + + internal IEnumerable Execute(int _, IEnumerable input) + { + return _func(input.Cast()).Cast(); + } + } + + /// + /// Helper to map the UDF for MapPartitionsWithIndex() to + /// . + /// + /// Input type + /// Output type + [UdfWrapper] + internal sealed class MapPartitionsWithIndexUdfWrapper + { + private readonly Func, IEnumerable> _func; + + internal MapPartitionsWithIndexUdfWrapper( + Func, IEnumerable> func) + { + _func = func; + } + + internal IEnumerable Execute(int pid, IEnumerable input) + { + return _func(pid, input.Cast()).Cast(); + } + } + + /// + /// Helper to map the UDF for Filter() to + /// . + /// + [UdfWrapper] + internal class FilterUdfWrapper + { + private readonly Func _func; + + internal FilterUdfWrapper(Func func) + { + _func = func; + } + + internal IEnumerable Execute(int _, IEnumerable input) + { + return input.Cast().Where(_func).Cast(); + } + } + } + + /// + /// PipelinedRDD is used to pipeline functions applied to RDD. + /// + /// Type of the elements in the RDD + internal sealed class PipelinedRDD : RDD, IJvmObjectReferenceProvider + { + private readonly RDD.WorkerFunction _func; + private readonly bool _preservesPartitioning; + + internal PipelinedRDD( + RDD.WorkerFunction func, + bool preservesPartitioning, + JvmObjectReference prevRddJvmObjRef, + SparkContext sparkContext, + SerializedMode prevSerializedMode) + : base(prevRddJvmObjRef, sparkContext, SerializedMode.Byte, prevSerializedMode) + { + _func = func ?? throw new ArgumentNullException("UDF cannot be null."); + _preservesPartitioning = preservesPartitioning; + } + + /// + /// Return a new RDD by applying a function to each partition of this RDD, + /// while tracking the index of the original partition. + /// + /// The element type of new RDD + /// The function to be applied to each partition + /// + /// Indicates if it preserves partition parameters + /// + /// A new RDD + internal override RDD MapPartitionsWithIndexInternal( + RDD.WorkerFunction.ExecuteDelegate newFunc, + bool preservesPartitioning = false) + { + if (IsPipelinable()) + { + RDD.WorkerFunction newWorkerFunc = RDD.WorkerFunction.Chain( + new RDD.WorkerFunction(_func.Func), + new RDD.WorkerFunction(newFunc)); + + return new PipelinedRDD( + newWorkerFunc, + preservesPartitioning && _preservesPartitioning, + _prevRddJvmObjRef, + _sparkContext, + _serializedMode); + } + + return base.MapPartitionsWithIndexInternal(newFunc, preservesPartitioning); + } + + /// + /// Returns the JVM reference for this RDD. It also initializes the reference + /// if it is not yet initialized. + /// + /// Note that PipelineRDD uses the JavaRDD internally. + /// + JvmObjectReference IJvmObjectReferenceProvider.Reference + { + get + { + if (_jvmObject == null) + { + IJvmBridge jvm = _prevRddJvmObjRef.Jvm; + + object rdd = _prevRddJvmObjRef.Invoke("rdd"); + byte[] command = Serialize(_func.Func, _prevSerializedMode, _serializedMode); + JvmObjectReference pythonFunction = + UdfUtils.CreatePythonFunction(jvm, command); + + var pythonRdd = (JvmObjectReference)jvm.CallStaticJavaMethod( + "org.apache.spark.api.dotnet.DotnetRDD", + "createPythonRDD", + rdd, + pythonFunction, + _preservesPartitioning); + + _jvmObject = (JvmObjectReference)pythonRdd.Invoke("asJavaRDD"); + } + + return _jvmObject; + } + } + + /// + /// Checks whether worker functions can be pipelined. + /// + /// True if worker functions can be pipelined. + private bool IsPipelinable() + { + return !_isCached && !_isCheckpointed; + } + } +} diff --git a/src/spark/Flowthru.Spark/RDD/Collector.cs b/src/spark/Flowthru.Spark/RDD/Collector.cs new file mode 100644 index 00000000..07041bb7 --- /dev/null +++ b/src/spark/Flowthru.Spark/RDD/Collector.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Utils; +using static Flowthru.Spark.Utils.CommandSerDe; + +namespace Flowthru.Spark.RDD +{ + /// + /// Collector collects objects from a socket. + /// + internal sealed class Collector + { + /// + /// Collects pickled row objects from the given socket. + /// + /// Stream object to read from + /// Serialized mode for each element + /// Collection of row objects + public IEnumerable Collect(Stream stream, SerializedMode serializedMode) + { + IDeserializer deserializer = GetDeserializer(serializedMode); + + int? length; + while (((length = SerDe.ReadBytesLength(stream)) != null) + && (length.GetValueOrDefault() > 0)) + { + yield return deserializer.Deserialize(stream, length.GetValueOrDefault()); + } + } + + /// + /// Returns a deserializer based on the given serialization mode. + /// + /// Serialization mode + /// A deserializer object + internal static IDeserializer GetDeserializer(SerializedMode mode) + { + switch (mode) + { + case SerializedMode.Byte: + return new BinaryDeserializer(); + case SerializedMode.String: + return new StringDeserializer(); + case SerializedMode.Row: + return new RowDeserializer(); + default: + throw new ArgumentException($"Unsupported mode found {mode}"); + } + } + + /// + /// Interface to deserialize an object from a given stream. + /// + internal interface IDeserializer + { + object Deserialize(Stream stream, int length); + } + + /// + /// Deserializer using the BinaryFormatter. + /// + private sealed class BinaryDeserializer : IDeserializer + { + public object Deserialize(Stream stream, int length) => + BinarySerDe.Deserialize(stream, length); + } + + /// + /// Deserializer for UTF-8 strings. + /// + private sealed class StringDeserializer : IDeserializer + { + public object Deserialize(Stream stream, int length) + { + return SerDe.ReadString(stream, length); + } + } + + /// + /// Deserializer for Pickled Rows. + /// + private sealed class RowDeserializer : IDeserializer + { + public object Deserialize(Stream stream, int length) + { + // Refer to the AutoBatchedPickler class in spark/core/src/main/scala/org/apache/ + // spark/api/python/SerDeUtil.scala regarding how the Rows may be batched. + return PythonSerDe.GetUnpickledObjects(stream, length).Cast().ToArray(); + } + } + } +} diff --git a/src/spark/Flowthru.Spark/RDD/WorkerFunction.cs b/src/spark/Flowthru.Spark/RDD/WorkerFunction.cs new file mode 100644 index 00000000..16c099e9 --- /dev/null +++ b/src/spark/Flowthru.Spark/RDD/WorkerFunction.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Flowthru.Spark.RDD +{ + /// + /// WorkerFunction provides the delegate type that is used for unifying + /// UDFs used for RDD. It also provides functionality to chain delegates. + /// + internal sealed class WorkerFunction + { + /// + /// Delegate type to which each RDD UDF is transformed. + /// + /// split id for the current task + /// enumerable collection of objects + /// enumerable collection of objects after applying UDF + internal delegate IEnumerable ExecuteDelegate( + int splitId, + IEnumerable input); + + public WorkerFunction(ExecuteDelegate func) + { + Func = func; + } + + internal ExecuteDelegate Func { get; } + + /// + /// Used to chain two function. + /// + internal static WorkerFunction Chain( + WorkerFunction innerFunction, + WorkerFunction outerFunction) + { + return new WorkerFunction( + new WorkerFuncChainHelper( + innerFunction.Func, + outerFunction.Func).Execute); + } + + /// + /// Helper to chain two delegates. + /// + [UdfWrapper] + private sealed class WorkerFuncChainHelper + { + private readonly ExecuteDelegate _innerFunc; + private readonly ExecuteDelegate _outerFunc; + + internal WorkerFuncChainHelper(ExecuteDelegate innerFunc, ExecuteDelegate outerFunc) + { + _innerFunc = innerFunc; + _outerFunc = outerFunc; + } + + internal IEnumerable Execute(int split, IEnumerable input) + { + return _outerFunc(split, _innerFunc(split, input)); + } + } + } +} diff --git a/src/spark/Flowthru.Spark/README.md b/src/spark/Flowthru.Spark/README.md new file mode 100644 index 00000000..e69de29b diff --git a/src/spark/Flowthru.Spark/Services/ConfigurationService.cs b/src/spark/Flowthru.Spark/Services/ConfigurationService.cs new file mode 100644 index 00000000..d516adb8 --- /dev/null +++ b/src/spark/Flowthru.Spark/Services/ConfigurationService.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using static System.Environment; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Services +{ + /// + /// Implementation of configuration service that helps getting config settings + /// to be used in .NET backend. + /// + internal sealed class ConfigurationService : IConfigurationService + { + public const string WorkerReadBufferSizeEnvVarName = "spark.dotnet.worker.readBufferSize"; + public const string WorkerWriteBufferSizeEnvVarName = + "spark.dotnet.worker.writeBufferSize"; + + internal const string DefaultWorkerDirEnvVarName = "DOTNET_WORKER_DIR"; + internal const string WorkerVerDirEnvVarNameFormat = "DOTNET_WORKER_{0}_DIR"; + + private const string DotnetBackendPortEnvVarName = "DOTNETBACKEND_PORT"; + private const int DotnetBackendDebugPort = 5567; + + private const string DotnetNumBackendThreadsEnvVarName = "DOTNET_SPARK_NUM_BACKEND_THREADS"; + private const int DotnetNumBackendThreadsDefault = 10; + + private static readonly string s_procBaseFileName = "Microsoft.Spark.Worker"; + + private readonly ILoggerService _logger = + LoggerServiceFactory.GetLogger(typeof(ConfigurationService)); + + private string _workerPath; + private string _workerDirEnvVarName; + + /// + /// The Microsoft.Spark.Worker filename. + /// + internal static string ProcFileName { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + $"{s_procBaseFileName}.exe" : + s_procBaseFileName; + + /// + /// Returns the environment variable name that defines the path to the + /// Microsoft.Spark.Worker. Using the Microsoft.Spark assembly version + /// we use the first environment variable that is defined in the + /// following order and default to DOTNET_WORKER_DIR if not: + /// + /// - DOTNET_WORKER_{MAJOR}_{MINOR}_{BUILD}_DIR + /// - DOTNET_WORKER_{MAJOR}_{MINOR}_DIR + /// - DOTNET_WORKER_{MAJOR}_DIR + /// + internal string WorkerDirEnvVarName + { + get + { + if (_workerDirEnvVarName != null) + { + return _workerDirEnvVarName; + } + + var version = new Version(AssemblyInfoProvider.MicrosoftSparkAssemblyInfo().AssemblyVersion); + var versionComponents = new int[] { version.Major, version.Minor, version.Build }; + for (int i = versionComponents.Length; i > 0; --i) + { + var span = new ReadOnlySpan(versionComponents, 0, i); + string verEnvVarName = string.Format( + WorkerVerDirEnvVarNameFormat, + string.Join("_", span.ToArray())); + if (!string.IsNullOrWhiteSpace(GetEnvironmentVariable(verEnvVarName))) + { + _workerDirEnvVarName = verEnvVarName; + return _workerDirEnvVarName; + } + } + + _workerDirEnvVarName = DefaultWorkerDirEnvVarName; + return _workerDirEnvVarName; + } + } + + /// + /// How often to run GC on JVM ThreadPool threads. Defaults to 5 minutes. + /// + public TimeSpan JvmThreadGCInterval + { + get + { + string envVar = GetEnvironmentVariable("DOTNET_JVM_THREAD_GC_INTERVAL"); + return string.IsNullOrEmpty(envVar) ? TimeSpan.FromMinutes(5) : TimeSpan.Parse(envVar); + } + } + + internal static bool IsDatabricks { get; } = + !string.IsNullOrEmpty(GetEnvironmentVariable("DATABRICKS_RUNTIME_VERSION")); + + /// + /// Returns the port number for socket communication between JVM and CLR. + /// + public int GetBackendPortNumber() + { + if (!int.TryParse( + GetEnvironmentVariable(DotnetBackendPortEnvVarName), + out int portNumber)) + { + _logger.LogInfo($"'{DotnetBackendPortEnvVarName}' environment variable is not set."); + portNumber = DotnetBackendDebugPort; + } + + _logger.LogInfo($"Using port {portNumber} for connection."); + + return portNumber; + } + + /// + /// Returns the max number of threads for socket communication between JVM and CLR. + /// + public int GetNumBackendThreads() + { + if (!int.TryParse( + GetEnvironmentVariable(DotnetNumBackendThreadsEnvVarName), + out int numThreads)) + { + numThreads = DotnetNumBackendThreadsDefault; + } + + return numThreads; + } + + /// + /// Returns the worker executable path. + /// + /// Worker executable path + public string GetWorkerExePath() + { + if (_workerPath != null) + { + return _workerPath; + } + + // If the WorkerDirEnvName environment variable is set, the worker path is constructed + // based on it. + string workerDir = GetEnvironmentVariable(WorkerDirEnvVarName); + if (!string.IsNullOrEmpty(workerDir)) + { + _workerPath = Path.Combine(workerDir, ProcFileName); + _logger.LogDebug( + "Using the {0} environment variable to construct .NET worker path: {1}.", + WorkerDirEnvVarName, + _workerPath); + return _workerPath; + } + + // Otherwise, the worker executable name is returned meaning it should be PATH. + _workerPath = ProcFileName; + return _workerPath; + } + + /// + /// Flag indicating whether running in REPL. + /// + public bool IsRunningRepl() => + EnvironmentUtils.GetEnvironmentVariableAsBool(Constants.RunningREPLEnvVar); + } +} diff --git a/src/spark/Flowthru.Spark/Services/ConsoleLoggerService.cs b/src/spark/Flowthru.Spark/Services/ConsoleLoggerService.cs new file mode 100644 index 00000000..ce50486c --- /dev/null +++ b/src/spark/Flowthru.Spark/Services/ConsoleLoggerService.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Flowthru.Spark.Services +{ + /// + /// This logger service will be used if the .NET driver app did not configure a logger. + /// Right now it just prints out the messages to Console + /// + internal sealed class ConsoleLoggerService : ILoggerService + { + internal static readonly ConsoleLoggerService s_instance = + new ConsoleLoggerService(typeof(Type)); + + private readonly Type _type; + + private ConsoleLoggerService(Type t) + { + _type = t; + } + + /// + /// Gets a value indicating whether logging is enabled for the Debug level. + /// Always return true for the DefaultLoggerService object. + /// + public bool IsDebugEnabled { get { return true; } } + + /// + /// Get an instance of ILoggerService by a given type of logger + /// + /// The type of a logger to return + /// An instance of ILoggerService + public ILoggerService GetLoggerInstance(Type type) + { + return new ConsoleLoggerService(type); + } + + /// + /// Logs a message at debug level. + /// + /// The message to be logged + public void LogDebug(string message) + { + Log("Debug", message); + } + + /// + /// Logs a message at debug level with a format string. + /// + /// The format string + /// The array of arguments + public void LogDebug(string messageFormat, params object[] messageParameters) + { + Log("Debug", string.Format(messageFormat, messageParameters)); + } + + /// + /// Logs a message at info level. + /// + /// The message to be logged + public void LogInfo(string message) + { + Log("Info", message); + } + + /// + /// Logs a message at info level with a format string. + /// + /// The format string + /// The array of arguments + public void LogInfo(string messageFormat, params object[] messageParameters) + { + Log("Info", string.Format(messageFormat, messageParameters)); + } + + /// + /// Logs a message at warning level. + /// + /// The message to be logged + public void LogWarn(string message) + { + Log("Warn", message); + } + + /// + /// Logs a message at warning level with a format string. + /// + /// The format string + /// The array of arguments + public void LogWarn(string messageFormat, params object[] messageParameters) + { + Log("Warn", string.Format(messageFormat, messageParameters)); + } + + /// + /// Logs a fatal message. + /// + /// The message to be logged + public void LogFatal(string message) + { + Log("Fatal", message); + } + + /// + /// Logs a fatal message with a format string. + /// + /// The format string + /// The array of arguments + public void LogFatal(string messageFormat, params object[] messageParameters) + { + Log("Fatal", string.Format(messageFormat, messageParameters)); + } + + /// + /// Logs a error message. + /// + /// The message to be logged + public void LogError(string message) + { + Log("Error", message); + } + + /// + /// Logs a error message with a format string. + /// + /// The format string + /// The array of arguments + public void LogError(string messageFormat, params object[] messageParameters) + { + Log("Error", string.Format(messageFormat, messageParameters)); + } + + /// + /// Logs an exception + /// + /// The exception to be logged + public void LogException(Exception e) + { + Log("Exception", $"{e.Message}{Environment.NewLine}{e.StackTrace}"); + } + + private void Log(string level, string message) + { + Console.WriteLine( + "[{0}] [{1}] [{2}] [{3}] {4}", + DateTime.UtcNow.ToString("o"), + Environment.MachineName, + level, + _type.Name, + message); + } + } +} diff --git a/src/spark/Flowthru.Spark/Services/IConfigurationService.cs b/src/spark/Flowthru.Spark/Services/IConfigurationService.cs new file mode 100644 index 00000000..96590586 --- /dev/null +++ b/src/spark/Flowthru.Spark/Services/IConfigurationService.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Flowthru.Spark.Services +{ + /// + /// Helps getting config settings to be used in .NET runtime + /// + internal interface IConfigurationService + { + /// + /// How often to run GC on JVM ThreadPool threads. + /// + TimeSpan JvmThreadGCInterval { get; } + + /// + /// The port number used for communicating with the .NET backend process. + /// + int GetBackendPortNumber(); + + /// + /// Returns the max number of threads for socket communication between JVM and CLR. + /// + int GetNumBackendThreads(); + + /// + /// The full path to the .NET worker executable. + /// + string GetWorkerExePath(); + + /// + /// Flag indicating whether running in REPL. + /// + bool IsRunningRepl(); + } +} diff --git a/src/spark/Flowthru.Spark/Services/ILoggerService.cs b/src/spark/Flowthru.Spark/Services/ILoggerService.cs new file mode 100644 index 00000000..0a7ee7d8 --- /dev/null +++ b/src/spark/Flowthru.Spark/Services/ILoggerService.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Flowthru.Spark.Services +{ + /// + /// Defines a logger what be used in service + /// + internal interface ILoggerService + { + /// + /// Gets a value indicating whether logging is enabled for the Debug level. + /// + bool IsDebugEnabled { get; } + + /// + /// Get an instance of ILoggerService by a given type of logger + /// + /// The type of a logger to return + /// An instance of ILoggerService + ILoggerService GetLoggerInstance(Type type); + + /// + /// Logs a message at debug level. + /// + /// The message to be logged + void LogDebug(string message); + + /// + /// Logs a message at debug level with a format string. + /// + /// The format string + /// The array of arguments + void LogDebug(string messageFormat, params object[] messageParameters); + + /// + /// Logs a message at info level. + /// + /// The message to be logged + void LogInfo(string message); + + /// + /// Logs a message at info level with a format string. + /// + /// The format string + /// The array of arguments + void LogInfo(string messageFormat, params object[] messageParameters); + + /// + /// Logs a message at warning level. + /// + /// The message to be logged + void LogWarn(string message); + + /// + /// Logs a message at warning level with a format string. + /// + /// The format string + /// The array of arguments + void LogWarn(string messageFormat, params object[] messageParameters); + + /// + /// Logs a fatal message. + /// + /// The message to be logged + void LogFatal(string message); + + /// + /// Logs a fatal message with a format string. + /// + /// The format string + /// The array of arguments + void LogFatal(string messageFormat, params object[] messageParameters); + + /// + /// Logs a error message. + /// + /// The message to be logged + void LogError(string message); + + /// + /// Logs a error message with a format string. + /// + /// The format string + /// The array of arguments + void LogError(string messageFormat, params object[] messageParameters); + + /// + /// Logs an exception + /// + /// The exception to be logged + void LogException(Exception e); + } +} diff --git a/src/spark/Flowthru.Spark/Services/LoggerServiceFactory.cs b/src/spark/Flowthru.Spark/Services/LoggerServiceFactory.cs new file mode 100644 index 00000000..74f9fc36 --- /dev/null +++ b/src/spark/Flowthru.Spark/Services/LoggerServiceFactory.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Flowthru.Spark.Services +{ + /// + /// Used to get logger service instances for different types + /// + internal class LoggerServiceFactory + { + private static Lazy s_loggerService = + new Lazy(() => GetDefaultLogger()); + + /// + /// Overrides an existing logger by a given logger service instance + /// + /// + /// The logger service instance used to overrides + /// + public static void SetLoggerService(ILoggerService loggerServiceOverride) + { + s_loggerService = new Lazy(() => loggerServiceOverride); + } + + /// + /// Gets an instance of logger service for a given type. + /// + /// The type of logger service to get + /// An instance of logger service + public static ILoggerService GetLogger(Type type) + { + return s_loggerService.Value.GetLoggerInstance(type); + } + + /// + /// if there exists xxx.exe.config file and log4net settings, then use log4net + /// + /// + private static ILoggerService GetDefaultLogger() + { + return ConsoleLoggerService.s_instance; + } + } +} diff --git a/src/spark/Flowthru.Spark/SparkConf.cs b/src/spark/Flowthru.Spark/SparkConf.cs new file mode 100644 index 00000000..61f257c7 --- /dev/null +++ b/src/spark/Flowthru.Spark/SparkConf.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark +{ + /// + /// Configuration for a Spark application. Used to set various Spark parameters as key-value + /// pairs. + /// + /// + /// Note that once a SparkConf object is passed to Spark, it is cloned and can no longer be + /// modified by the user. Spark does not support modifying the configuration at runtime. + /// + public sealed class SparkConf : IJvmObjectReferenceProvider + { + /// + /// Constructor. + /// + /// + /// Indicates whether to load values from Java system properties + /// + public SparkConf(bool loadDefaults = true) + : this(SparkEnvironment.JvmBridge.CallConstructor( + "org.apache.spark.SparkConf", + loadDefaults)) + { + } + + internal SparkConf(JvmObjectReference jvmObject) + { + Reference = jvmObject; + + // Special handling for debug mode because spark.master and spark.app.name will not + // be set in debug mode. Driver code may override these values if SetMaster or + // SetAppName methods are used. + if (string.IsNullOrWhiteSpace(Get("spark.master", ""))) + { + SetMaster("local"); + } + if (string.IsNullOrWhiteSpace(Get("spark.app.name", ""))) + { + SetAppName("debug app"); + } + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// The master URL to connect to, such as "local" to run locally with one thread, + /// "local[4]" to run locally with 4 cores, or "spark://master:7077" to run on a Spark + /// standalone cluster. + /// + /// Spark master + public SparkConf SetMaster(string master) + { + Reference.Invoke("setMaster", master); + return this; + } + + /// + /// Set a name for your application. Shown in the Spark web UI. + /// + /// Name of the app + public SparkConf SetAppName(string appName) + { + Reference.Invoke("setAppName", appName); + return this; + } + + /// + /// Set the location where Spark is installed on worker nodes. + /// + /// + /// + public SparkConf SetSparkHome(string sparkHome) + { + Reference.Invoke("setSparkHome", sparkHome); + return this; + } + + /// + /// Set the value of a string config + /// + /// Config name + /// Config value + public SparkConf Set(string key, string value) + { + Reference.Invoke("set", key, value); + return this; + } + + /// + /// Get a int parameter value, falling back to a default if not set + /// + /// Key to use + /// Default value to use + public int GetInt(string key, int defaultValue) + { + return (int)Reference.Invoke("getInt", key, defaultValue); + } + + /// + /// Get a string parameter value, falling back to a default if not set + /// + /// Key to use + /// Default value to use + public string Get(string key, string defaultValue) + { + return (string)Reference.Invoke("get", key, defaultValue); + } + + /// + /// Get all parameters as a list of pairs. + /// + public IReadOnlyList> GetAll() + { + var kvpStringCollection = (string)Reference.Jvm.CallStaticJavaMethod( + "org.apache.spark.sql.api.dotnet.JvmBridgeUtils", + "getSparkConfAsString", + this); + + string[] kvpStringArray = kvpStringCollection.Split(';'); + var configs = new List>(); + + foreach (string kvpString in kvpStringArray) + { + if (!string.IsNullOrEmpty(kvpString)) + { + string[] kvpItems = kvpString.Split('='); + if ((kvpItems.Length == 2) && + !string.IsNullOrEmpty(kvpItems[0]) && + !string.IsNullOrEmpty(kvpItems[1])) + { + configs.Add(new KeyValuePair(kvpItems[0], kvpItems[1])); + } + } + } + + return configs; + } + } +} diff --git a/src/spark/Flowthru.Spark/SparkContext.cs b/src/spark/Flowthru.Spark/SparkContext.cs new file mode 100644 index 00000000..74961fcd --- /dev/null +++ b/src/spark/Flowthru.Spark/SparkContext.cs @@ -0,0 +1,391 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using Flowthru.Spark.Hadoop.Conf; +using Flowthru.Spark.Interop.Internal.Scala; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Utils; +using static Flowthru.Spark.Utils.CommandSerDe; + +namespace Flowthru.Spark +{ + /// + /// Main entry point for Spark functionality. A SparkContext represents the connection + /// to a Spark cluster, and can be used to create RDDs, accumulators and broadcast + /// variables on that cluster. + /// + /// Only one `SparkContext` should be active per JVM. You must `stop()` the + /// active `SparkContext` before creating a new one. + /// + public sealed class SparkContext : IJvmObjectReferenceProvider + { + private readonly SparkConf _conf; + + /// + /// Create a SparkContext object with the given config. + /// + /// a Spark config object describing the application configuration. + /// Any settings in this config overrides the default configs as well as system properties. + /// + public SparkContext(SparkConf conf) + : this(conf.Reference.Jvm.CallConstructor("org.apache.spark.SparkContext", conf)) + { + } + + /// + /// Create a SparkContext that loads settings from system properties (for instance, + /// when launching with spark-submit). + /// + public SparkContext() + : this(new SparkConf()) + { + } + + /// + /// Alternative constructor that allows setting common Spark properties directly. + /// + /// Cluster URL to connect to (e.g. spark://host:port, local) + /// A name for the application + /// + /// A object specifying other Spark parameters + /// + public SparkContext(string master, string appName, SparkConf conf) + : this(GetUpdatedConf(master, appName, null, conf)) + { + } + + /// + /// Initializes a SparkContext instance with a specific master and application name. + /// + /// Cluster URL to connect to (e.g. spark://host:port, local) + /// A name for the application + public SparkContext(string master, string appName) + : this(GetUpdatedConf(master, appName, null, null)) + { + } + + /// + /// Alternative constructor that allows setting common Spark properties directly. + /// + /// Cluster URL to connect to (e.g. spark://host:port, local) + /// A name for the application + /// The path that holds spark bits + public SparkContext(string master, string appName, string sparkHome) + : this(GetUpdatedConf(master, appName, sparkHome, null)) + { + } + + /// + /// Constructor where SparkContext object is already created. + /// + /// JVM object reference for this SparkContext object + internal SparkContext(JvmObjectReference jvmObject) + { + Reference = jvmObject; + _conf = new SparkConf((JvmObjectReference)Reference.Invoke("getConf")); + } + + + public JvmObjectReference Reference { get; private set; } + + /// + /// Returns SparkConf object associated with this SparkContext object. + /// Note that modifying the SparkConf object will not have any impact. + /// + /// SparkConf object + public SparkConf GetConf() => _conf; + + /// + /// This function may be used to get or instantiate a SparkContext and register it as a + /// singleton object. Because we can only have one active SparkContext per JVM, + /// this is useful when applications may wish to share a SparkContext. + /// + /// that will be used for creating SparkContext + /// + /// + /// Current SparkContext (or a new one if it wasn't created before the function call) + /// + public static SparkContext GetOrCreate(SparkConf conf) + { + IJvmBridge jvm = conf.Reference.Jvm; + return new SparkContext( + (JvmObjectReference)jvm.CallStaticJavaMethod( + "org.apache.spark.SparkContext", + "getOrCreate", + conf)); + } + + /// + /// Control our logLevel. This overrides any user-defined log settings. + /// + /// + /// Valid log levels include: ALL, DEBUG, ERROR, FATAL, INFO, OFF, TRACE, WARN + /// + /// The desired log level as a string. + public void SetLogLevel(string logLevel) + { + Reference.Invoke("setLogLevel", logLevel); + } + + /// + /// Shut down the SparkContext. + /// + public void Stop() + { + Reference.Invoke("stop"); + } + + /// + /// Default level of parallelism to use when not given by user (e.g. Parallelize()). + /// + public int DefaultParallelism => (int)Reference.Invoke("defaultParallelism"); + + /// + /// Creates a modified version of with the parameters that can be + /// passed separately to SparkContext, to make it easier to write SparkContext's + /// constructors. + /// + /// Cluster URL to connect to (e.g. spark://host:port, local) + /// A name for the application + /// The path that holds spark bits + /// + /// A object specifying other Spark parameters + /// + /// Modified object. + private static SparkConf GetUpdatedConf( + string master, + string appName, + string sparkHome, + SparkConf conf) + { + SparkConf sparkConf = conf ?? new SparkConf(); + if (master != null) + { + sparkConf.SetMaster(master); + } + if (appName != null) + { + sparkConf.SetAppName(appName); + } + if (sparkHome != null) + { + sparkConf.SetSparkHome(sparkHome); + } + + return sparkConf; + } + + /// + /// Sets a human readable description of the current job. + /// + /// Description of the current job + public void SetJobDescription(string value) + { + Reference.Invoke("setJobDescription", value); + } + + /// + /// Assigns a group ID to all the jobs started by this thread until the group ID is set to + /// a different value or cleared. + /// + /// + /// Often, a unit of execution in an application consists of multiple Spark actions or + /// jobs. Application programmers can use this method to group all those jobs together + /// and give a group description. Once set, the Spark web UI will associate such jobs + /// with this group. + /// + /// Group Id + /// Description on the job group + /// + /// If true, then job cancellation will result in `Thread.interrupt()` being called on the + /// job's executor threads. + /// + public void SetJobGroup(string groupId, string description, bool interruptOnCancel = false) + { + Reference.Invoke("setJobGroup", groupId, description, interruptOnCancel); + } + + /// + /// Clear the current thread's job group ID and its description. + /// + public void ClearJobGroup() + { + Reference.Invoke("clearJobGroup"); + } + + /// + /// Distribute a local collection to form an RDD. + /// + /// Type of the elements in the collection + /// Collection to distribute + /// Number of partitions to divide the collection into + /// RDD representing distributed collection + internal RDD Parallelize(IEnumerable seq, int? numSlices = null) + { + using var memoryStream = new MemoryStream(); + + var values = new List(); + foreach (T obj in seq) + { + BinarySerDe.Serialize(memoryStream, obj); + values.Add(memoryStream.ToArray()); + memoryStream.SetLength(0); + } + + return new RDD( + (JvmObjectReference)Reference.Jvm.CallStaticJavaMethod( + "org.apache.spark.api.dotnet.DotnetRDD", + "createJavaRDDFromArray", + Reference, + values, + numSlices ?? DefaultParallelism), + this, + SerializedMode.Byte); + } + + /// + /// Read a text file from HDFS, a local file system (available on all nodes), or any + /// Hadoop-supported file system URI, and return it as an RDD of strings. + /// + /// path to the text file on a supported file system + /// minimum number of partitions for the resulting RDD + /// RDD of lines of the text file + internal RDD TextFile(string path, int? minPartitions = null) + { + return new RDD( + WrapAsJavaRDD((JvmObjectReference)Reference.Invoke( + "textFile", + path, + minPartitions ?? DefaultParallelism)), + this, + SerializedMode.String); + } + + /// + /// Add a file to be downloaded with this Spark job on every node. + /// + /// + /// If a file is added during execution, it will not be available until the next + /// TaskSet starts. + /// + /// A path can be added only once. Subsequent additions of the same path are ignored. + /// + /// + /// File path can be either a local file, a file in HDFS (or other Hadoop-supported + /// filesystems), or an HTTP, HTTPS or FTP URI. + /// + /// + /// If true, a directory can be given in `path`. Currently directories are supported + /// only for Hadoop-supported filesystems. + /// + public void AddFile(string path, bool recursive = false) + { + Reference.Invoke("addFile", path, recursive); + } + + /// + /// Returns a list of file paths that are added to resources. + /// + /// File paths that are added to resources. + public IEnumerable ListFiles() => + new Seq((JvmObjectReference)Reference.Invoke("listFiles")); + + /// + /// Add an archive to be downloaded and unpacked with this Spark job on every node. + /// + /// + /// If an archive is added during execution, it will not be available until the next + /// TaskSet starts. + /// + /// A path can be added only once. Subsequent additions of the same path are ignored. + /// + /// + /// Archive path can be either a local file, a file in HDFS (or other Hadoop-supported + /// filesystems), or an HTTP, HTTPS or FTP URI. The given path should be one of .zip, + /// .tar, .tar.gz, .tgz and .jar. + /// + [Since(Versions.V3_1_0)] + public void AddArchive(string path) + { + Reference.Invoke("addArchive", path); + } + + /// + /// Returns a list of archive paths that are added to resources. + /// + /// Archive paths that are added to resources. + [Since(Versions.V3_1_0)] + public IEnumerable ListArchives() => + new Seq((JvmObjectReference)Reference.Invoke("listArchives")); + + /// + /// Sets the directory under which RDDs are going to be checkpointed. + /// + /// + /// path to the directory where checkpoint files will be stored + /// + public void SetCheckpointDir(string directory) + { + Reference.Invoke("setCheckpointDir", directory); + } + + /// + /// Return the directory where RDDs are checkpointed. + /// + /// + /// The directory where RDDs are checkpointed. Returns `null` if no checkpoint + /// directory has been set. + /// + public string GetCheckpointDir() + { + return (string)new Option((JvmObjectReference)Reference.Invoke("getCheckpointDir")).OrNull(); + } + + /// + /// Broadcast a read-only variable to the cluster, returning a Microsoft.Spark.Broadcast + /// object for reading it in distributed functions. The variable will be sent to each + /// executor only once. + /// + /// Type of the variable being broadcast + /// Value of the broadcast variable + /// A Broadcast object of type + public Broadcast Broadcast(T value) + { + return new Broadcast(this, value); + } + + /// + /// A default Hadoop Configuration for the Hadoop code (e.g. file systems) that we reuse. + /// + /// The Hadoop Configuration. + public Configuration HadoopConfiguration() => + new Configuration((JvmObjectReference)Reference.Invoke("hadoopConfiguration")); + + /// + /// Returns JVM object reference to JavaRDD object transformed + /// from a Scala RDD object. + /// + /// + /// The transformation is for easy reflection on the JVM side. + /// + /// JVM object reference to Scala RDD + /// JVM object reference to JavaRDD object + private JvmObjectReference WrapAsJavaRDD(JvmObjectReference rdd) + { + return (JvmObjectReference)Reference.Jvm.CallStaticJavaMethod( + "org.apache.spark.api.dotnet.DotnetRDD", + "toJavaRDD", + rdd); + } + /// + /// Returns a string that represents the version of Spark on which this application is running. + /// + /// + /// A string that represents the version of Spark on which this application is running. + /// + public string Version() => (string)Reference.Invoke("version"); + } +} diff --git a/src/spark/Flowthru.Spark/SparkFiles.cs b/src/spark/Flowthru.Spark/SparkFiles.cs new file mode 100644 index 00000000..08d6a39a --- /dev/null +++ b/src/spark/Flowthru.Spark/SparkFiles.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark +{ + /// + /// Resolves paths to files added through . + /// + public static class SparkFiles + { + private static IJvmBridge Jvm { get; } = SparkEnvironment.JvmBridge; + private static readonly string s_sparkFilesClassName = "org.apache.spark.SparkFiles"; + + [ThreadStatic] + private static string s_rootDirectory; + + [ThreadStatic] + private static bool s_isRunningOnWorker; + + /// + /// Get the absolute path of a file added through + /// . + /// + /// The name of the file added through + /// . + /// + /// The absolute path of the file. + public static string Get(string fileName) => + Path.GetFullPath(Path.Combine(GetRootDirectory(), fileName)); + + /// + /// Get the root directory that contains files added through + /// . + /// + /// The root directory that contains the files. + public static string GetRootDirectory() => + s_isRunningOnWorker ? + s_rootDirectory : + (string)Jvm.CallStaticJavaMethod(s_sparkFilesClassName, "getRootDirectory"); + + /// + /// Set the root directory that contains files added through + /// . + /// + /// This should only be called from the Microsoft.Spark.Worker. + /// + /// + /// Root directory that contains files added + /// through . + /// + internal static void SetRootDirectory(string path) + { + s_isRunningOnWorker = true; + s_rootDirectory = path; + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/ArrowArrayHelpers.cs b/src/spark/Flowthru.Spark/Sql/ArrowArrayHelpers.cs new file mode 100644 index 00000000..494c84c0 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/ArrowArrayHelpers.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Apache.Arrow; +using Apache.Arrow.Types; +using Microsoft.Data.Analysis; + +namespace Flowthru.Spark.Sql +{ + /// + /// Helper methods to work with Apache Arrow arrays. + /// + internal static class ArrowArrayHelpers + { + private static readonly HashSet s_twoBufferArrowTypes = new HashSet() + { + ArrowTypeId.Boolean, + ArrowTypeId.Int8, + ArrowTypeId.UInt8, + ArrowTypeId.Int16, + ArrowTypeId.UInt16, + ArrowTypeId.Int32, + ArrowTypeId.UInt32, + ArrowTypeId.Int64, + ArrowTypeId.UInt64, + ArrowTypeId.Float, + ArrowTypeId.Double, + ArrowTypeId.Date32, + ArrowTypeId.Date64, + ArrowTypeId.Timestamp, + }; + + private static readonly HashSet s_threeBufferArrowTypes = new HashSet() + { + ArrowTypeId.String, + ArrowTypeId.Binary, + }; + + public static DataFrameColumn CreateEmptyColumn() + { + Type type = typeof(T); + return type switch + { + _ when type == typeof(BooleanDataFrameColumn) => + new BooleanDataFrameColumn("Empty"), + _ when type == typeof(SByteDataFrameColumn) => + new SByteDataFrameColumn("Empty"), + _ when type == typeof(ByteDataFrameColumn) => + new ByteDataFrameColumn("Empty"), + _ when type == typeof(Int16DataFrameColumn) => + new Int16DataFrameColumn("Empty"), + _ when type == typeof(UInt16DataFrameColumn) => + new UInt16DataFrameColumn("Empty"), + _ when type == typeof(Int32DataFrameColumn) => + new Int32DataFrameColumn("Empty"), + _ when type == typeof(UInt32DataFrameColumn) => + new UInt32DataFrameColumn("Empty"), + _ when type == typeof(Int64DataFrameColumn) => + new Int64DataFrameColumn("Empty"), + _ when type == typeof(UInt64DataFrameColumn) => + new UInt64DataFrameColumn("Empty"), + _ when type == typeof(SingleDataFrameColumn) => + new SingleDataFrameColumn("Empty"), + _ when type == typeof(DoubleDataFrameColumn) => + new DoubleDataFrameColumn("Empty"), + _ when type == typeof(ArrowStringDataFrameColumn) => + new ArrowStringDataFrameColumn("Empty"), + _ => throw new NotSupportedException($"Unknown type: {type}") + }; + } + + public static IArrowArray CreateEmptyArray(IArrowType arrowType) + { + ArrayData data = BuildEmptyArrayDataFromArrowType(arrowType); + return ArrowArrayFactory.BuildArray(data); + } + + public static IArrowArray CreateEmptyArray() + { + ArrayData data = BuildEmptyArrayDataFromArrayType(); + return ArrowArrayFactory.BuildArray(data); + } + + private static ArrayData BuildEmptyArrayDataFromArrayType() + { + Type type = typeof(T); + return type switch + { + _ when type == typeof(BooleanArray) => + BuildEmptyArrayDataFromArrowType(BooleanType.Default), + _ when type == typeof(Int8Array) => + BuildEmptyArrayDataFromArrowType(Int8Type.Default), + _ when type == typeof(UInt8Array) => + BuildEmptyArrayDataFromArrowType(UInt8Type.Default), + _ when type == typeof(Int16Array) => + BuildEmptyArrayDataFromArrowType(Int16Type.Default), + _ when type == typeof(UInt16Array) => + BuildEmptyArrayDataFromArrowType(UInt16Type.Default), + _ when type == typeof(Int32Array) => + BuildEmptyArrayDataFromArrowType(Int32Type.Default), + _ when type == typeof(UInt32Array) => + BuildEmptyArrayDataFromArrowType(UInt32Type.Default), + _ when type == typeof(Int64Array) => + BuildEmptyArrayDataFromArrowType(Int64Type.Default), + _ when type == typeof(UInt64Array) => + BuildEmptyArrayDataFromArrowType(UInt64Type.Default), + _ when type == typeof(FloatArray) => + BuildEmptyArrayDataFromArrowType(FloatType.Default), + _ when type == typeof(DoubleArray) => + BuildEmptyArrayDataFromArrowType(DoubleType.Default), + _ when type == typeof(Date64Array) => + BuildEmptyArrayDataFromArrowType(Date64Type.Default), + _ when type == typeof(TimestampArray) => + BuildEmptyArrayDataFromArrowType(TimestampType.Default), + _ when type == typeof(StringArray) => + BuildEmptyArrayDataFromArrowType(StringType.Default), + _ when type == typeof(BinaryArray) => + BuildEmptyArrayDataFromArrowType(BinaryType.Default), + + _ => throw new NotSupportedException($"Unknown type: {type}") + }; + } + + private static ArrayData BuildEmptyArrayDataFromArrowType(IArrowType arrowType) + { + if (s_twoBufferArrowTypes.Contains(arrowType.TypeId)) + { + return new ArrayData(arrowType, 0, + buffers: new[] { ArrowBuffer.Empty, ArrowBuffer.Empty }); + } + + if (s_threeBufferArrowTypes.Contains(arrowType.TypeId)) + { + return new ArrayData(arrowType, 0, + buffers: new[] { ArrowBuffer.Empty, ArrowBuffer.Empty, ArrowBuffer.Empty }); + } + + throw new NotSupportedException($"Unsupported type: {arrowType.TypeId}"); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/ArrowFunctions.cs b/src/spark/Flowthru.Spark/Sql/ArrowFunctions.cs new file mode 100644 index 00000000..04674524 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/ArrowFunctions.cs @@ -0,0 +1,274 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Apache.Arrow; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Sql +{ + /// + /// Functions available for DataFrame operations. + /// + public static class ArrowFunctions + { + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf(Func udf) + where T : IArrowArray + where TResult : IArrowArray + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + UdfUtils.CreateVectorUdfWrapper(udf)).Apply1; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf(Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where TResult : IArrowArray + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + UdfUtils.CreateVectorUdfWrapper(udf)).Apply2; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where TResult : IArrowArray + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + UdfUtils.CreateVectorUdfWrapper(udf)).Apply3; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where TResult : IArrowArray + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + UdfUtils.CreateVectorUdfWrapper(udf)).Apply4; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where TResult : IArrowArray + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + UdfUtils.CreateVectorUdfWrapper(udf)).Apply5; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where TResult : IArrowArray + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + UdfUtils.CreateVectorUdfWrapper(udf)).Apply6; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where TResult : IArrowArray + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + UdfUtils.CreateVectorUdfWrapper(udf)).Apply7; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where TResult : IArrowArray + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + UdfUtils.CreateVectorUdfWrapper(udf)).Apply8; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where T9 : IArrowArray + where TResult : IArrowArray + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + UdfUtils.CreateVectorUdfWrapper(udf)).Apply9; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the type of the tenth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where T9 : IArrowArray + where T10 : IArrowArray + where TResult : IArrowArray + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + UdfUtils.CreateVectorUdfWrapper(udf)).Apply10; + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/ArrowGroupedMapUdfWrapper.cs b/src/spark/Flowthru.Spark/Sql/ArrowGroupedMapUdfWrapper.cs new file mode 100644 index 00000000..3da93602 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/ArrowGroupedMapUdfWrapper.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Apache.Arrow; + +namespace Flowthru.Spark.Sql +{ + /// + /// Wraps the given Func object, which represents a Grouped Map UDF. + /// + /// + /// UDF serialization requires a "wrapper" object in order to serialize/deserialize. + /// + [UdfWrapper] + internal sealed class ArrowGroupedMapUdfWrapper + { + private readonly Func _func; + + internal ArrowGroupedMapUdfWrapper(Func func) + { + _func = func; + } + + internal RecordBatch Execute(RecordBatch input) + { + return _func(input); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/ArrowUdfWrapper.cs b/src/spark/Flowthru.Spark/Sql/ArrowUdfWrapper.cs new file mode 100644 index 00000000..90bebbf2 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/ArrowUdfWrapper.cs @@ -0,0 +1,465 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Apache.Arrow; +using static Flowthru.Spark.Sql.ArrowArrayHelpers; + +namespace Flowthru.Spark.Sql +{ + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class ArrowUdfWrapper + where T : IArrowArray + where TResult : IArrowArray + { + private readonly Func _func; + + internal ArrowUdfWrapper(Func func) + { + _func = func; + } + + internal IArrowArray Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + int length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T)columns[argOffsets[0]]); + } + + return CreateEmptyArray(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class ArrowUdfWrapper + where T1 : IArrowArray + where T2 : IArrowArray + where TResult : IArrowArray + { + private readonly Func _func; + + internal ArrowUdfWrapper(Func func) + { + _func = func; + } + + internal IArrowArray Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + int length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]]); + } + + return CreateEmptyArray(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class ArrowUdfWrapper + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where TResult : IArrowArray + { + private readonly Func _func; + + internal ArrowUdfWrapper(Func func) + { + _func = func; + } + + internal IArrowArray Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + int length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]]); + } + + return CreateEmptyArray(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class ArrowUdfWrapper + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where TResult : IArrowArray + { + private readonly Func _func; + + internal ArrowUdfWrapper(Func func) + { + _func = func; + } + + internal IArrowArray Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + int length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]]); + } + + return CreateEmptyArray(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class ArrowUdfWrapper + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where TResult : IArrowArray + { + private readonly Func _func; + + internal ArrowUdfWrapper(Func func) + { + _func = func; + } + + internal IArrowArray Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + int length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]]); + } + + return CreateEmptyArray(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class ArrowUdfWrapper + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where TResult : IArrowArray + { + private readonly Func _func; + + internal ArrowUdfWrapper(Func func) + { + _func = func; + } + + internal IArrowArray Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + int length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]], + (T6)columns[argOffsets[5]]); + } + + return CreateEmptyArray(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class ArrowUdfWrapper + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where TResult : IArrowArray + { + private readonly Func _func; + + internal ArrowUdfWrapper(Func func) + { + _func = func; + } + + internal IArrowArray Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + int length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]], + (T6)columns[argOffsets[5]], + (T7)columns[argOffsets[6]]); + } + + return CreateEmptyArray(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class ArrowUdfWrapper + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where TResult : IArrowArray + { + private readonly Func _func; + + internal ArrowUdfWrapper(Func func) + { + _func = func; + } + + internal IArrowArray Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + int length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]], + (T6)columns[argOffsets[5]], + (T7)columns[argOffsets[6]], + (T8)columns[argOffsets[7]]); + } + + return CreateEmptyArray(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class ArrowUdfWrapper + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where T9 : IArrowArray + where TResult : IArrowArray + { + private readonly Func _func; + + internal ArrowUdfWrapper(Func func) + { + _func = func; + } + + internal IArrowArray Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + int length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]], + (T6)columns[argOffsets[5]], + (T7)columns[argOffsets[6]], + (T8)columns[argOffsets[7]], + (T9)columns[argOffsets[8]]); + } + + return CreateEmptyArray(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the type of the tenth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class ArrowUdfWrapper + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where T9 : IArrowArray + where T10 : IArrowArray + where TResult : IArrowArray + { + private readonly Func _func; + + internal ArrowUdfWrapper(Func func) + { + _func = func; + } + + internal IArrowArray Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + int length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]], + (T6)columns[argOffsets[5]], + (T7)columns[argOffsets[6]], + (T8)columns[argOffsets[7]], + (T9)columns[argOffsets[8]], + (T10)columns[argOffsets[9]]); + } + + return CreateEmptyArray(); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Avro/Functions.cs b/src/spark/Flowthru.Spark/Sql/Avro/Functions.cs new file mode 100644 index 00000000..6eeb381c --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Avro/Functions.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql.Avro +{ + /// + /// Functions for serialization and deserialization of data in Avro format. + /// + public static class Functions + { + private static IJvmBridge Jvm { get; } = SparkEnvironment.JvmBridge; + private static readonly Lazy s_avroClassName = + new Lazy(() => + { + Version sparkVersion = SparkEnvironment.SparkVersion; + return sparkVersion.Major switch + { + 2 => "org.apache.spark.sql.avro.package", + 3 => "org.apache.spark.sql.avro.functions", + 4 => "org.apache.spark.sql.avro.functions", + _ => throw new NotSupportedException($"Spark {sparkVersion} not supported.") + }; + }); + + /// + /// Converts a binary column of avro format into its corresponding catalyst value. The specified + /// schema must match the read data, otherwise the behavior is undefined: it may fail or return + /// arbitrary result. + /// + /// The binary column. + /// The avro schema in JSON string format. + /// Column object + [Since(Versions.V2_4_0)] + public static Column FromAvro(Column data, string jsonFormatSchema) => + new Column( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_avroClassName.Value, + "from_avro", + data, + jsonFormatSchema)); + + /// + /// Converts a binary column of avro format into its corresponding catalyst value. The specified + /// schema must match the read data, otherwise the behavior is undefined: it may fail or return + /// arbitrary result. To deserialize the data with a compatible and evolved schema, the expected Avro + /// schema can be set via the option avroSchema. + /// + /// The binary column. + /// The avro schema in JSON string format. + /// Options to control how the Avro record is parsed. + /// Column object + [Since(Versions.V3_0_0)] + public static Column FromAvro( + Column data, + string jsonFormatSchema, + Dictionary options) => + new Column( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_avroClassName.Value, + "from_avro", + data, + jsonFormatSchema, + options)); + + /// + /// Converts a column into binary of avro format. + /// + /// The data column. + /// Column object + [Since(Versions.V2_4_0)] + public static Column ToAvro(Column data) => + new Column((JvmObjectReference)Jvm.CallStaticJavaMethod(s_avroClassName.Value, "to_avro", data)); + + /// + /// Converts a column into binary of avro format. + /// + /// The data column. + /// User-specified output avro schema in JSON string format. + /// Column object + [Since(Versions.V3_0_0)] + public static Column ToAvro(Column data, string jsonFormatSchema) => + new Column( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_avroClassName.Value, + "to_avro", + data, + jsonFormatSchema)); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Builder.cs b/src/spark/Flowthru.Spark/Sql/Builder.cs new file mode 100644 index 00000000..460f16b4 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Builder.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql +{ + /// + /// The entry point to programming Spark with the Dataset and DataFrame API. + /// + public sealed class Builder : IJvmObjectReferenceProvider + { + private readonly Dictionary _options = + new Dictionary() { { "spark.app.kind", "sparkdotnet" } }; + + internal Builder() + : this( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + "org.apache.spark.sql.SparkSession", + "builder")) + { + } + + internal Builder(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Sets the Spark master URL to connect to, such as "local" to run locally, "local[4]" to + /// run locally with 4 cores, or "spark://master:7077" to run on a Spark standalone + /// cluster. + /// + /// Master URL + public Builder Master(string master) + { + Config("spark.master", master); + return this; + } + + /// + /// Sets a name for the application, which will be shown in the Spark web UI. + /// If no application name is set, a randomly generated name will be used. + /// + /// Name of the app + public Builder AppName(string appName) + { + Config("spark.app.name", appName); + return this; + } + + /// + /// Sets a config option. Options set using this method are automatically propagated to + /// both SparkConf and SparkSession's own configuration. + /// + /// Key for the configuration + /// value of the configuration + public Builder Config(string key, string value) + { + _options[key] = value; + return this; + } + + /// + /// Sets a config option. Options set using this method are automatically propagated to + /// both SparkConf and SparkSession's own configuration. + /// + /// Key for the configuration + /// value of the configuration + public Builder Config(string key, bool value) + { + _options[key] = value.ToString(); + return this; + } + + /// + /// Sets a config option. Options set using this method are automatically propagated to + /// both SparkConf and SparkSession's own configuration. + /// + /// Key for the configuration + /// value of the configuration + public Builder Config(string key, double value) + { + _options[key] = value.ToString(); + return this; + } + + /// + /// Sets a config option. Options set using this method are automatically propagated to + /// both SparkConf and SparkSession's own configuration. + /// + /// Key for the configuration + /// value of the configuration + public Builder Config(string key, long value) + { + _options[key] = value.ToString(); + return this; + } + + /// + /// Sets a list of config options based on the given SparkConf + /// + public Builder Config(SparkConf conf) + { + foreach (KeyValuePair keyValuePair in conf.GetAll()) + { + _options[keyValuePair.Key] = keyValuePair.Value; + } + + return this; + } + + /// + /// Enables Hive support, including connectivity to a persistent Hive metastore, support + /// for Hive serdes, and Hive user-defined functions. + /// + public Builder EnableHiveSupport() + { + return Config("spark.sql.catalogImplementation", "hive"); + } + + /// + /// Gets an existing [[SparkSession]] or, if there is no existing one, creates a new + /// one based on the options set in this builder. + /// + /// + public SparkSession GetOrCreate() + { + var sparkConf = new SparkConf(); + foreach (KeyValuePair option in _options) + { + sparkConf.Set(option.Key, option.Value); + } + + Reference.Invoke("config", sparkConf); + + return new SparkSession((JvmObjectReference)Reference.Invoke("getOrCreate")); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Catalog/Catalog.cs b/src/spark/Flowthru.Spark/Sql/Catalog/Catalog.cs new file mode 100644 index 00000000..b1789038 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Catalog/Catalog.cs @@ -0,0 +1,460 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.Sql.Catalog +{ + /// + /// Catalog interface for Spark. To access this, use SparkSession.Catalog. + /// + public sealed class Catalog : IJvmObjectReferenceProvider + { + internal Catalog(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Caches the specified table in-memory. + /// + /// Spark SQL can cache tables using an in-memory columnar format by calling + /// `CacheTable("tableName")` or `DataFrame.Cache()`. Spark SQL will scan only required + /// columns and will automatically tune compression to minimize memory usage and GC + /// pressure. You can call `UncacheTable("tableName")` to remove the table from memory. + /// + /// Is either a qualified or unqualified name that designates a + /// table. If no database identifier is provided, it refers to a table in the current + /// database. + public void CacheTable(string tableName) => Reference.Invoke("cacheTable", tableName); + + /// + /// Removes all cached tables from the in-memory cache. You can either clear all cached + /// tables at once using this or clear each table individually using + /// `UncacheTable("tableName")`. + /// + public void ClearCache() => Reference.Invoke("clearCache"); + + /// + /// Creates a table, in the hive warehouse, from the given path and returns the + /// corresponding DataFrame. The table will contain the contents of the parquet + /// file that is in the `path` parameter. The default data source type is parquet. This can + /// be changed using `CreateTable(tableName, path, source)` or setting the configuration + /// option `spark.sql.sources.default` when creating the spark session using + /// `Config("spark.sql.sources.default", "csv")` or after you have created the session using + /// `Conf().Set("spark.sql.sources.default", "csv")`. + /// + /// The name of the table to create. + /// Path to use to create the table. + /// The contents of the files in the path parameter as a `DataFrame`. + public DataFrame CreateTable(string tableName, string path) => + new DataFrame((JvmObjectReference)Reference.Invoke("createTable", tableName, path)); + + /// + /// Creates a table, in the hive warehouse, from the given path based from a data + /// source and returns the corresponding DataFrame. + /// + /// The type of file type (csv, parquet, etc.) is specified using the `source` parameter. + /// + /// Is either a qualified or unqualified name that designates a + /// table. If no database identifier is provided, it refers to a table in the current + /// database. + /// Path to use to create the table. + /// Data source to use to create the table such as parquet, csv, etc. + /// + /// The results of reading the files in path as a `DataFrame`. + public DataFrame CreateTable(string tableName, string path, string source) => + new DataFrame( + (JvmObjectReference)Reference.Invoke("createTable", tableName, path, source)); + + /// + /// Creates a table based on the dataset in a data source and a set of options. + /// + /// + /// Is either a qualified or unqualified name that designates a table. If no database + /// identifier is provided, it refers to a table in the current database. + /// + /// + /// Data source to use to create the table such as parquet, csv, etc. + /// + /// Options used to table + /// The corresponding DataFrame + public DataFrame CreateTable(string tableName, string source, IDictionary options) => + new DataFrame((JvmObjectReference)Reference.Invoke("createTable", tableName, source, options)); + + /// + /// Creates a table based on the dataset in a data source and a set of options. + /// + /// + /// Is either a qualified or unqualified name that designates a table. If no database + /// identifier is provided, it refers to a table in the current database. + /// + /// + /// Data source to use to create the table such as parquet, csv, etc. + /// + /// Description of the table + /// Options used to table + /// The corresponding DataFrame + [Since(Versions.V3_1_0)] + public DataFrame CreateTable( + string tableName, + string source, + string description, + IDictionary options) => + new DataFrame( + (JvmObjectReference)Reference.Invoke( + "createTable", tableName, source, description, options)); + + /// + /// Create a table based on the dataset in a data source, a schema and a set of options. + /// + /// + /// Is either a qualified or unqualified name that designates a table. If no database + /// identifier is provided, it refers to a table in the current database. + /// + /// + /// Data source to use to create the table such as parquet, csv, etc. + /// + /// Schema of the table + /// Options used to table + /// The corresponding DataFrame + public DataFrame CreateTable( + string tableName, + string source, + StructType schema, + IDictionary options) => + new DataFrame( + (JvmObjectReference)Reference.Invoke( + "createTable", + tableName, + source, + DataType.FromJson(Reference.Jvm, schema.Json), + options)); + + /// + /// Create a table based on the dataset in a data source, a schema and a set of options. + /// + /// + /// Is either a qualified or unqualified name that designates a table. If no database + /// identifier is provided, it refers to a table in the current database. + /// + /// + /// Data source to use to create the table such as parquet, csv, etc. + /// + /// Schema of the table + /// Description of the table + /// Options used to table + /// The corresponding DataFrame + [Since(Versions.V3_1_0)] + public DataFrame CreateTable( + string tableName, + string source, + StructType schema, + string description, + IDictionary options) => + new DataFrame( + (JvmObjectReference)Reference.Invoke( + "createTable", + tableName, + source, + DataType.FromJson(Reference.Jvm, schema.Json), + description, + options)); + + /// + /// Returns the current database in this session. By default your session will be + /// connected to the "default" database (named "default") and to change database + /// either use `SetCurrentDatabase("databaseName")` or + /// `SparkSession.Sql("USE DATABASE databaseName")`. + /// + /// The database name as a string. + public string CurrentDatabase() => (string)Reference.Invoke("currentDatabase"); + + /// + /// Check if the database with the specified name exists. This will check the list + /// of hive databases in the current session to see if the database exists. + /// + /// Name of the database to check. + /// bool, true if the database exists and false if it does not exist. + public bool DatabaseExists(string dbName) => + (bool)Reference.Invoke("databaseExists", dbName); + + /// + /// Drops the global temporary view with the given view name in the catalog. + /// + /// You can create global temporary views by taking a DataFrame and calling + /// `DataFrame.CreateOrReplaceGlobalTempView`. + /// + /// The unqualified name of the temporary view to be dropped. + /// + /// bool, true if the view was dropped and false if it was not dropped. + public bool DropGlobalTempView(string viewName) => + (bool)Reference.Invoke("dropGlobalTempView", viewName); + + /// + /// Drops the local temporary view with the given view name in the catalog. + /// Local temporary view is session-scoped. Its lifetime is the lifetime of the session + /// that created it, i.e. it will be automatically dropped when the session terminates. + /// It's not tied to any databases, i.e. we can't use db1.view1 to reference a local + /// temporary view. + /// + /// You can create temporary views by taking a DataFrame and calling + /// `DataFrame.CreateOrReplaceTempView`. + /// + /// The unqualified name of the temporary view to be dropped. + /// + /// bool, true if the view was dropped and false if it was not dropped. + public bool DropTempView(string viewName) => + (bool)Reference.Invoke("dropTempView", viewName); + + /// + /// Check if the function with the specified name exists. `FunctionsExists` includes in-built + /// functions such as `abs`. To see if a built-in function exists you must use the + /// unqualified name. If you create a function you can use the qualified name. + /// + /// Is either a qualified or unqualified name that designates a + /// function. If no database identifier is provided, it refers to a function in the + /// current database. + /// bool, true if the function exists and false it is does not. + public bool FunctionExists(string functionName) => + (bool)Reference.Invoke("functionExists", functionName); + + /// + /// Check if the function with the specified name exists in the specified database. If you + /// want to check if a built-in function exists specify the dbName as null or use + /// `FunctionExists(functionName)`. + /// + /// Is a name that designates a database. + /// Is an unqualified name that designates a function. + /// bool, true if the function exists and false it is does not. + public bool FunctionExists(string dbName, string functionName) => + (bool)Reference.Invoke("functionExists", dbName, functionName); + + /// + /// Get the database with the specified name. + /// + /// Calling `GetDatabase` gives you access to the hive database name, description and + /// location. + /// + /// Name of the database to get. + /// `Database` object which includes the name, description and locationUri of + /// the database. + public Database GetDatabase(string dbName) => + new Database((JvmObjectReference)Reference.Invoke("getDatabase", dbName)); + + /// + /// Get the function with the specified name. This function can be a temporary function + /// or a function. + /// + /// Is either a qualified or unqualified name that designates a + /// function. It follows the same resolution rule with SQL: search for built-in/temp + /// functions first then functions in the current database(namespace). + /// `Function` object which includes the class name, database, description, + /// whether it is temporary and the name of the function. + public Function GetFunction(string functionName) => + new Function((JvmObjectReference)Reference.Invoke("getFunction", functionName)); + + /// + /// Get the function with the specified name in the specified database under the Hive + /// Metastore. + /// To get built-in functions, or functions in other catalogs, please use `getFunction(functionName)` with + /// qualified function name instead. + /// + /// Is a name that designates a database. Built-in functions will be + /// in database null rather than default. + /// Is an unqualified name that designates a function in the + /// specified database. + /// `Function` object which includes the class name, database, description, + /// whether it is temporary and the name of the function. + public Function GetFunction(string dbName, string functionName) => + new Function( + (JvmObjectReference)Reference.Invoke("getFunction", dbName, functionName)); + + /// + /// Get the table or view with the specified name. You can use this to find the tables + /// description, database, type and whether it is a temporary table or not. + /// + /// Is either a qualified or unqualified name that designates a + /// table. If no database identifier is provided, it refers to a table in the current + /// database. + /// `Table` object which includes name, database, description, table type and + /// whether the table is temporary or not. + public Table GetTable(string tableName) => + new Table((JvmObjectReference)Reference.Invoke("getTable", tableName)); + + /// + /// Get the table or view with the specified name in the specified database. You can use + /// this to find the tables description, database, type and whether it is a temporary + /// table or not. + /// + /// Is a name that designates a database. + /// Is an unqualified name that designates a table in the specified + /// database. + /// `Table` object which includes name, database, description, table type and + /// whether the table is temporary or not. + public Table GetTable(string dbName, string tableName) => + new Table((JvmObjectReference)Reference.Invoke("getTable", dbName, tableName)); + + /// + /// Returns true if the table is currently cached in-memory. If the table is cached then it + /// will consume memory. To remove the table from cache use `UncacheTable` or `ClearCache` + /// + /// Is either a qualified or unqualified name that designates a + /// table. If no database identifier is provided, it refers to a table in the current + /// database. + /// bool, true if the table is cahced and false if it is not cached + public bool IsCached(string tableName) => (bool)Reference.Invoke("isCached", tableName); + + /// + /// Returns a list of columns for the given table/view or temporary view. The DataFrame + /// includes the name, description, dataType, whether it is nullable or if it is + /// partitioned and if it is broken in buckets. + /// + /// Is either a qualified or unqualified name that designates a + /// table. If no database identifier is provided, it refers to a table in the current + /// database. + /// `DataFrame` with the name, description, dataType, whether each column is + /// nullable, if the column is partitioned and if the column is broken in buckets. + /// + public DataFrame ListColumns(string tableName) => + new DataFrame((JvmObjectReference)Reference.Invoke("listColumns", tableName)); + + /// + /// Returns a list of columns for the given table/view in the specified database. + /// The `DataFrame` includes the name, description, dataType, whether it is nullable or if it + /// is partitioned and if it is broken in buckets. + /// + /// Is a name that designates a database. + /// Is an unqualified name that designates a table in the specified + /// database. + /// `DataFrame` with the name, description, dataType, whether each column is + /// nullable, if the column is partitioned and if the column is broken in buckets. + /// + public DataFrame ListColumns(string dbName, string tableName) => + new DataFrame((JvmObjectReference)Reference.Invoke("listColumns", dbName, tableName)); + + /// + /// Returns a list of databases available across all sessions. The `DataFrame` contains + /// the name, description and locationUri of each database. + /// + /// `DataFrame` with the name, description and locationUri of each database. + /// + public DataFrame ListDatabases() => + new DataFrame((JvmObjectReference)Reference.Invoke("listDatabases")); + + /// + /// Returns a list of functions registered in the current database. This includes all + /// temporary functions. The `DataFrame` contains the class name, database, description, + /// whether it is temporary and the name of each function. + /// + /// `DataFrame` with the class name, database, description, whether it is + /// temporary and the name of each function. + public DataFrame ListFunctions() => + new DataFrame((JvmObjectReference)Reference.Invoke("listFunctions")); + + /// + /// Returns a list of functions registered in the specified database. This includes all + /// temporary functions. The `DataFrame` contains the class name, database, description, + /// whether it is temporary and the name of the function. + /// + /// Is a name that designates a database. + /// `DataFrame` with the class name, database, description, whether it is + /// temporary and the name of each function. + public DataFrame ListFunctions(string dbName) => + new DataFrame((JvmObjectReference)Reference.Invoke("listFunctions", dbName)); + + /// + /// Returns a list of tables/views in the current database. The `DataFrame` includes the + /// name, database, description, table type and whether the table is temporary or not. + /// + /// `DataFrame` with the name, database, description, table type and whether the + /// table is temporary or not for each table in the default database + public DataFrame ListTables() => + new DataFrame((JvmObjectReference)Reference.Invoke("listTables")); + + /// + /// Returns a list of tables/views in the specified database. The `DataFrame` includes the + /// name, database, description, table type and whether the table is temporary or not. + /// + /// Is a name that designates a database. + /// `DataFrame` with the name, database, description, table type and whether the + /// table is temporary or not for each table in the named database + public DataFrame ListTables(string dbName) => + new DataFrame((JvmObjectReference)Reference.Invoke("listTables", dbName)); + + /// + /// Recovers all the partitions in the directory of a table and update the catalog. This + /// only works for partitioned tables and not un-partitioned tables or views. + /// + /// Is either a qualified or unqualified name that designates a + /// table. If no database identifier is provided, it refers to a table in the current + /// database. + public void RecoverPartitions(string tableName) => + Reference.Invoke("recoverPartitions", tableName); + + /// + /// Invalidates and refreshes all the cached data (and the associated metadata) for any + /// Dataset that contains the given data source path. Path matching is by prefix, + /// i.e. "/" would invalidate everything that is cached. + /// + /// Path to refresh + public void RefreshByPath(string path) => Reference.Invoke("refreshByPath", path); + + /// + /// Invalidates and refreshes all the cached data and metadata of the given table. For + /// performance reasons, Spark SQL or the external data source library it uses might cache + /// certain metadata about a table, such as the location of blocks. When those change + /// outside of Spark SQL, users should call this function to invalidate the cache. If this + /// table is cached as an InMemoryRelation, drop the original cached version and make the + /// new version cached lazily. + /// + /// Is either a qualified or unqualified name that designates a + /// table. If no database identifier is provided, it refers to a table in the current + /// database. + public void RefreshTable(string tableName) => Reference.Invoke("refreshTable", tableName); + + /// + /// Sets the current default database in this session. + /// + /// The name of the database to set. + public void SetCurrentDatabase(string dbName) => + Reference.Invoke("setCurrentDatabase", dbName); + + /// + /// Check if the table or view with the specified name exists. This can either be a + /// temporary view or a table/view. + /// + /// Is either a qualified or unqualified name that designates a + /// table. If no database identifier is provided, it refers to a table in the current + /// database. + /// bool, true if the table exists and false if it does not exist + public bool TableExists(string tableName) => + (bool)Reference.Invoke("tableExists", tableName); + + /// + /// Check if the table or view with the specified name exists in the specified database. + /// + /// Is a name that designates a database. + /// Is an unqualified name that designates a table. + /// bool, true if the table exists in the specified database and false if it does + /// not exist + public bool TableExists(string dbName, string tableName) => + (bool)Reference.Invoke("tableExists", dbName, tableName); + + /// + /// Removes the specified table from the in-memory cache. + /// + /// Is either a qualified or unqualified name that designates a + /// table. If no database identifier is provided, it refers to a table in the current + /// database. + /// + /// To cache a table use `CacheTable(tableName)`. + /// + public void UncacheTable(string tableName) => Reference.Invoke("uncacheTable", tableName); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Catalog/Database.cs b/src/spark/Flowthru.Spark/Sql/Catalog/Database.cs new file mode 100644 index 00000000..758ef407 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Catalog/Database.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql.Catalog +{ + /// + /// A database in Spark, as returned by the `ListDatabases` method defined in `Catalog`. + /// + public sealed class Database : IJvmObjectReferenceProvider + { + internal Database(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Description of the database. + /// + /// string, the description of the database. + public string Description => (string)Reference.Invoke("description"); + + /// + /// Path (in the form of a uri) to data files + /// + /// string, the location of the database. + public string LocationUri => (string)Reference.Invoke("locationUri"); + + /// + /// Name of the database. + /// + /// string, the name of the database. + public string Name => (string)Reference.Invoke("name"); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Catalog/Function.cs b/src/spark/Flowthru.Spark/Sql/Catalog/Function.cs new file mode 100644 index 00000000..b30028d1 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Catalog/Function.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql.Catalog +{ + /// + /// A user-defined function in Spark, as returned by `ListFunctions` method in `Catalog`. + /// + public sealed class Function : IJvmObjectReferenceProvider + { + internal Function(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Name of the database the function belongs to. + /// + /// string, the name of the database that the function is in. + public string Database => (string)Reference.Invoke("database"); + + /// + /// Description of the function. + /// + /// string, the description of the function. + public string Description => (string)Reference.Invoke("description"); + + /// + /// Whether the function is temporary or not + /// + /// bool, true if the function is temporary and false if it is not temporary. + /// + public bool IsTemporary => (bool)Reference.Invoke("isTemporary"); + + /// + /// Name of the function + /// + /// string, the name of the function. + public string Name => (string)Reference.Invoke("name"); + + /// + /// The fully qualified class name of the function + /// + /// string, the name of the class that implements the function. + public string ClassName => (string)Reference.Invoke("className"); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Catalog/Table.cs b/src/spark/Flowthru.Spark/Sql/Catalog/Table.cs new file mode 100644 index 00000000..742aaeac --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Catalog/Table.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql.Catalog +{ + /// + /// A table in Spark, as returned by the `ListTables` method in `Catalog`. + /// + public sealed class Table : IJvmObjectReferenceProvider + { + internal Table(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Name of the database the table belongs to. + /// + /// string, the name of the database the table is in. + public string Database => (string)Reference.Invoke("database"); + + /// + /// Description of the table. + /// + /// string, the description of the table. + public string Description => (string)Reference.Invoke("description"); + + /// + /// Whether the table is temporary or not. + /// + /// bool, true if the table is temporary, false if it is not. + public bool IsTemporary => (bool)Reference.Invoke("isTemporary"); + + /// + /// The name of the table. + /// + /// string, the name of the table. + public string Name => (string)Reference.Invoke("name"); + + /// + /// The type of table (e.g. view/table) + /// + /// string, will return either `view` or `table` + public string TableType => (string)Reference.Invoke("tableType"); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Column.cs b/src/spark/Flowthru.Spark/Sql/Column.cs new file mode 100644 index 00000000..8aa782f2 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Column.cs @@ -0,0 +1,967 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql.Expressions; + +namespace Flowthru.Spark.Sql +{ + /// + /// Column class represents a column that will be computed based on the data in a DataFrame. + /// + public sealed class Column : IJvmObjectReferenceProvider + { + + /// + /// Constructor for Column class. + /// + /// JVM object reference + internal Column(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Negate the given column. + /// + /// Column to negate + /// New column after applying negation + public static Column operator -(Column self) + { + return ApplyFunction(self, "negate"); + } + + /// + /// Apply inversion of boolean expression, i.e. NOT. + /// + /// Column to apply inversion + /// New column after applying inversion + public static Column operator !(Column self) + { + return ApplyFunction(self, "not"); + } + + /// + /// Apply equality test on the given two columns. + /// + /// Column on the left side of equality test + /// Column on the right side of equality test + /// New column after applying equality test + public static Column operator ==(Column lhs, object rhs) + { + return lhs.EqualTo(rhs); + } + + /// + /// Equality test. + /// + /// The right hand side of expression being tested for equality + /// New column after applying the equal to operator + public Column EqualTo(object rhs) + { + return ApplyMethod("equalTo", rhs); + } + + /// + /// Apply inequality test. + /// + /// Column on the left side of inequality test + /// Column on the right side of inequality test + /// New column after applying inequality test + public static Column operator !=(Column lhs, object rhs) + { + return lhs.NotEqual(rhs); + } + + /// + /// Inequality test. + /// + /// + /// The right hand side of expression being tested for inequality. + /// + /// New column after applying not equal operator + public Column NotEqual(object rhs) + { + return ApplyMethod("notEqual", rhs); + } + + /// + /// Apply "greater than" operator for the given two columns. + /// + /// Column on the left side of the operator + /// Column on the right side of the operator + /// New column after applying the operator + public static Column operator >(Column lhs, object rhs) + { + return lhs.Gt(rhs); + } + + /// + /// Greater than. + /// + /// + /// The object that is in comparison to test if the left hand side is greater. + /// + /// New column after applying the greater than operator + public Column Gt(object rhs) + { + return ApplyMethod("gt", rhs); + } + + /// + /// Apply "less than" operator for the given two columns. + /// + /// Column on the left side of the operator + /// Column on the right side of the operator + /// New column after applying the operator + public static Column operator <(Column lhs, object rhs) + { + return lhs.Lt(rhs); + } + + /// + /// Less than. + /// + /// + /// The object that is in comparison to test if the left hand side is lesser. + /// + /// New column after applying the less than operator + public Column Lt(object rhs) + { + return ApplyMethod("lt", rhs); + } + + /// + /// Apply "less than or equal to" operator for the given two columns. + /// + /// Column on the left side of the operator + /// Column on the right side of the operator + /// New column after applying the operator + public static Column operator <=(Column lhs, object rhs) + { + return lhs.Leq(rhs); + } + + /// + /// Less than or equal to. + /// + /// + /// The object that is in comparison to test if the left hand side is less or equal to. + /// + /// New column after applying the less than or equal to operator + public Column Leq(object rhs) + { + return ApplyMethod("leq", rhs); + } + + /// + /// Apply "greater than or equal to" operator for the given two columns. + /// + /// Column on the left side of the operator + /// Column on the right side of the operator + /// New column after applying the operator + public static Column operator >=(Column lhs, object rhs) + { + return lhs.Geq(rhs); + } + + /// + /// Greater or equal to. + /// + /// + /// The object that is in comparison to test if the left hand side is greater or equal to + /// + /// New column after applying the greater or equal to operator + public Column Geq(object rhs) + { + return ApplyMethod("geq", rhs); + } + + /// + /// Apply equality test that is safe for null values. + /// + /// Object to apply equality test + /// New column after applying the equality test + public Column EqNullSafe(object obj) + { + return ApplyMethod("eqNullSafe", obj); + } + + /// + /// Evaluates a condition and returns one of multiple possible result expressions. + /// If Otherwise(object) is not defined at the end, null is returned for + /// unmatched conditions. This method can be chained with other 'when' invocations in case + /// multiple matches are required. + /// + /// The condition to check + /// The value to set if the condition is true + /// New column after applying the when method + public Column When(Column condition, object value) + { + return ApplyMethod("when", condition, value); + } + + /// + /// Evaluates a list of conditions and returns one of multiple possible result expressions. + /// If otherwise is not defined at the end, null is returned for unmatched conditions. + /// This is used when the When(Column, object) method is applied. + /// + /// The value to set + /// New column after applying otherwise method + public Column Otherwise(object value) + { + return ApplyMethod("otherwise", value); + } + + /// + /// True if the current column is between the lower bound and upper bound, inclusive. + /// + /// The lower bound + /// The upper bound + /// New column after applying the between method + public Column Between(object lowerBound, object upperBound) + { + return ApplyMethod("between", lowerBound, upperBound); + } + + /// + /// True if the current expression is NaN. + /// + /// + /// New column with values true if the preceding column had a NaN + /// value in the same index, and false otherwise. + /// + public Column IsNaN() + { + return ApplyMethod("isNaN"); + } + + /// + /// True if the current expression is null. + /// + /// + /// New column with values true if the preceding column had a null + /// value in the same index, and false otherwise. + /// + public Column IsNull() + { + return ApplyMethod("isNull"); + } + + /// + /// True if the current expression is NOT null. + /// + /// + /// New column with values true if the preceding column had a non-null + /// value in the same index, and false otherwise. + /// + public Column IsNotNull() + { + return ApplyMethod("isNotNull"); + } + + /// + /// Apply boolean OR operator for the given two columns. + /// + /// Column on the left side of the operator + /// Column on the right side of the operator + /// New column after applying the operator + public static Column operator |(Column lhs, Column rhs) + { + // Check the comment for operator & why rhs is Column instead of object. + return lhs.Or(rhs); + } + + /// + /// Apply boolean OR operator with the given column. + /// + /// Column to apply OR operator + /// New column after applying the operator + public Column Or(Column other) + { + return ApplyMethod("or", other); + } + + /// + /// Apply boolean AND operator for the given two columns. + /// + /// Column on the left side of the operator + /// Column on the right side of the operator + /// New column after applying the operator + public static Column operator &(Column lhs, Column rhs) + { + // Note that in Spark, && is overloaded which takes "Any" for the rhs. + // Since the overloaded operator on JVM cannot be reflected/called, + // this is calling "and" instead, which takes in "Column" for the rhs. + return lhs.And(rhs); + } + + /// + /// Apply boolean AND operator with the given column. + /// + /// Column to apply AND operator + /// New column after applying the operator + public Column And(Column other) + { + return ApplyMethod("and", other); + } + + /// + /// Apply sum of two expressions. + /// + /// Column on the left side of the operator + /// Object on the right side of the operator + /// New column after applying the sum operation + public static Column operator +(Column lhs, object rhs) + { + return lhs.Plus(rhs); + } + + /// + /// Sum of this expression and another expression. + /// + /// The expression to be summed with + /// New column after applying the plus operator + public Column Plus(object rhs) + { + return ApplyMethod("plus", rhs); + } + + /// + /// Apply subtraction of two expressions. + /// + /// Column on the left side of the operator + /// Object on the right side of the operator + /// New column after applying the subtraction operation + public static Column operator -(Column lhs, object rhs) + { + return lhs.Minus(rhs); + } + + /// + /// Subtraction. Subtract the other expression from this expression. + /// + /// The expression to be subtracted with + /// New column after applying the minus operator + public Column Minus(object rhs) + { + return ApplyMethod("minus", rhs); + } + + /// + /// Apply multiplication of two expressions. + /// + /// Column on the left side of the operator + /// Object on the right side of the operator + /// New column after applying the multiplication operation + public static Column operator *(Column lhs, object rhs) + { + return lhs.Multiply(rhs); + } + + /// + /// Multiplication of this expression and another expression. + /// + /// The expression to be multiplied with + /// New column after applying the multiply operator + public Column Multiply(object rhs) + { + return ApplyMethod("multiply", rhs); + } + + /// + /// Apply division of two expressions. + /// + /// Column on the left side of the operator + /// Object on the right side of the operator + /// New column after applying the division operation + public static Column operator /(Column lhs, object rhs) + { + return lhs.Divide(rhs); + } + + /// + /// Division of this expression by another expression. + /// + /// The expression to be divided by + /// New column after applying the divide operator + public Column Divide(object rhs) + { + return ApplyMethod("divide", rhs); + } + + /// + /// Apply division of two expressions. + /// + /// Column on the left side of the operator + /// Object on the right side of the operator + /// New column after applying the division operation + public static Column operator %(Column lhs, object rhs) + { + return lhs.Mod(rhs); + } + + /// + /// Modulo (a.k.a remainder) expression. + /// + /// + /// The expression to be divided by to get the remainder for. + /// + /// New column after applying the mod operator + public Column Mod(object rhs) + { + return ApplyMethod("mod", rhs); + } + + /// + /// SQL like expression. Returns a boolean column based on a SQL LIKE match. + /// + /// The literal that is used to compute the SQL LIKE match + /// New column after applying the SQL LIKE match + public Column Like(string literal) + { + return ApplyMethod("like", literal); + } + + /// + /// SQL RLIKE expression (LIKE with Regex). Returns a boolean column based on a regex + /// match. + /// + /// The literal that is used to compute the Regex match + /// New column after applying the regex LIKE method + public Column RLike(string literal) + { + return ApplyMethod("rlike", literal); + } + + /// + /// An expression that gets an item at position `ordinal` out of an array, + /// or gets a value by key `key` in a `MapType`. + /// + /// The key with which to identify the item + /// New column after getting an item given a specific key + public Column GetItem(object key) + { + return ApplyMethod("getItem", key); + } + + /// + /// An expression that adds/replaces field in by name. + /// + /// The name of the field + /// Column to assign to the field + /// + /// New column after adding/replacing field in by name. + /// + [Since(Versions.V3_1_0)] + public Column WithField(string fieldName, Column column) + { + return ApplyMethod("withField", fieldName, column); + } + + /// + /// An expression that drops fields in by name. + /// + /// Name of fields to drop. + /// New column after after dropping fields. + [Since(Versions.V3_1_0)] + public Column DropFields(params string[] fieldNames) + { + return ApplyMethod("dropFields", fieldNames); + } + + /// + /// An expression that gets a field by name in a `StructType`. + /// + /// The name of the field + /// New column after getting a field for a specific key + public Column GetField(string fieldName) + { + return ApplyMethod("getField", fieldName); + } + + /// + /// An expression that returns a substring. + /// + /// Expression for the starting position + /// Expression for the length of the substring + /// + /// New column that is bound by the start position provided, and the length. + /// + public Column SubStr(Column startPos, Column len) + { + return ApplyMethod("substr", startPos, len); + } + + /// + /// An expression that returns a substring. + /// + /// Starting position + /// Length of the substring + /// + /// New column that is bound by the start position provided, and the length. + /// + public Column SubStr(int startPos, int len) + { + return ApplyMethod("substr", startPos, len); + } + + /// + /// Contains the other element. Returns a boolean column based on a string match. + /// + /// + /// The object that is used to check for existence in the current column. + /// + /// New column after checking if the column contains object other + public Column Contains(object other) + { + return ApplyMethod("contains", other); + } + + /// + /// String starts with. Returns a boolean column based on a string match. + /// + /// + /// The other column containing strings with which to check how values + /// in this column starts. + /// + /// + /// A boolean column where entries are true if values in the current + /// column does indeed start with the values in the given column. + /// + public Column StartsWith(Column other) + { + return ApplyMethod("startsWith", other); + } + + /// + /// String starts with another string literal. + /// Returns a boolean column based on a string match. + /// + /// + /// The string literal used to check how values in a column starts. + /// + /// + /// A boolean column where entries are true if values in the current column + /// does indeed start with the given string literal. + /// + public Column StartsWith(string literal) + { + return ApplyMethod("startsWith", literal); + } + + /// + /// String ends with. Returns a boolean column based on a string match. + /// + /// + /// The other column containing strings with which to check how values + /// in this column ends. + /// + /// + /// A boolean column where entries are true if values in the current + /// column does indeed end with the values in the given column. + /// + public Column EndsWith(Column other) + { + return ApplyMethod("endsWith", other); + } + + /// + /// String ends with another string literal. Returns a boolean column based + /// on a string match. + /// + /// + /// The string literal used to check how values in a column ends. + /// + /// + /// A boolean column where entries are true if values in the current column + /// does indeed end with the given string literal. + /// + public Column EndsWith(string literal) + { + return ApplyMethod("endsWith", literal); + } + + /// + /// Gives the column an alias. Same as `As()`. + /// + /// The alias that is given + /// New column after applying an alias + public Column Alias(string alias) + { + return ApplyMethod("alias", alias); + } + + /// + /// Gives the column an alias. + /// + /// The alias that is given + /// New column after applying the as alias operator + public Column As(string alias) + { + return Alias(alias); + } + + /// + /// Assigns the given aliases to the results of a table generating function. + /// + /// A list of aliases + /// Column object + public Column As(IEnumerable alias) + { + return ApplyMethod("as", alias); + } + + /// + /// Extracts a value or values from a complex type. + /// The following types of extraction are supported: + /// + /// 1. Given an Array, an integer ordinal can be used to retrieve a single value. + /// 2. Given a Map, a key of the correct type can be used to retrieve an individual value. + /// 3. Given a Struct, a string fieldName can be used to extract that field. + /// 4. Given an Array of Structs, a string fieldName can be used to extract field + /// of every struct in that array, and return an Array of fields. + /// + /// + /// Object used to extract value(s) from the column + /// Column object + public Column Apply(object extraction) + { + return ApplyMethod("apply", extraction); + } + + /// + /// Gives the column a name (alias). + /// + /// Alias column name + /// Column object + public Column Name(string alias) + { + return ApplyMethod("name", alias); + } + + /// + /// Casts the column to a different data type, using the canonical string + /// representation of the type. + /// + /// + /// The supported types are: `string`, `boolean`, `byte`, `short`, `int`, `long`, + /// `float`, `double`, `decimal`, `date`, `timestamp`. + /// + /// String version of datatype + /// Column object + public Column Cast(string to) + { + return ApplyMethod("cast", to); + } + + /// + /// Returns a sort expression based on ascending order of the column, + /// and null values return before non-null values. + /// + /// New column after applying the descending order operator + public Column Desc() + { + return ApplyMethod("desc"); + } + + /// + /// Returns a sort expression based on the descending order of the column, + /// and null values appear before non-null values. + /// + /// Column object + public Column DescNullsFirst() + { + return ApplyMethod("desc_nulls_first"); + } + + /// + /// Returns a sort expression based on the descending order of the column, + /// and null values appear after non-null values. + /// + /// Column object + public Column DescNullsLast() + { + return ApplyMethod("desc_nulls_last"); + } + + /// + /// Returns a sort expression based on ascending order of the column. + /// + /// New column after applying the ascending order operator + public Column Asc() + { + return ApplyMethod("asc"); + } + + /// + /// Returns a sort expression based on ascending order of the column, + /// and null values return before non-null values. + /// + /// + public Column AscNullsFirst() + { + return ApplyMethod("asc_nulls_first"); + } + + /// + /// Returns a sort expression based on ascending order of the column, + /// and null values appear after non-null values. + /// + /// + public Column AscNullsLast() + { + return ApplyMethod("asc_nulls_last"); + } + + /// + /// Prints the expression to the console for debugging purposes. + /// + /// To print extended version or not + public void Explain(bool extended) + { + ApplyMethod("explain", extended); + } + + /// + /// Compute bitwise OR of this expression with another expression. + /// + /// + /// The other column that will be used to compute the bitwise OR. + /// + /// New column after applying bitwise OR operator + public Column BitwiseOR(object other) + { + return ApplyMethod("bitwiseOR", other); + } + + /// + /// Compute bitwise AND of this expression with another expression. + /// + /// + /// The other column that will be used to compute the bitwise AND. + /// + /// New column after applying the bitwise AND operator + public Column BitwiseAND(object other) + { + return ApplyMethod("bitwiseAND", other); + } + + /// + /// Compute bitwise XOR of this expression with another expression. + /// + /// + /// The other column that will be used to compute the bitwise XOR. + /// + /// New column after applying bitwise XOR operator + public Column BitwiseXOR(object other) + { + return ApplyMethod("bitwiseXOR", other); + } + + /// + /// Defines a windowing column. + /// + /// + /// A window specification that defines the partitioning, ordering, and frame boundaries. + /// + /// Column object + public Column Over(WindowSpec window) + { + return ApplyMethod("over", window); + } + + /// + /// Defines an empty analytic clause. In this case the analytic function is applied + /// and presented for all rows in the result set. + /// + /// Column object + public Column Over() + { + return ApplyMethod("over"); + } + + /// + /// A boolean expression that is evaluated to true if the value of this expression + /// is contained by the evaluated values of the arguments. + /// + /// List of values to check the column against + /// Column object + public Column IsIn(params string[] list) + { + return ApplyMethod("isin", list); + } + + /// + /// A boolean expression that is evaluated to true if the value of this expression + /// is contained by the evaluated values of the arguments. + /// + /// List of values to check the column against + /// Column object + public Column IsIn(params int[] list) + { + return ApplyMethod("isin", list); + } + + /// + /// A boolean expression that is evaluated to true if the value of this expression + /// is contained by the evaluated values of the arguments. + /// + /// List of values to check the column against + /// Column object + public Column IsIn(params long[] list) + { + return ApplyMethod("isin", list); + } + + /// + /// A boolean expression that is evaluated to true if the value of this expression + /// is contained by the evaluated values of the arguments. + /// + /// List of values to check the column against + /// Column object + public Column IsIn(params bool[] list) + { + return ApplyMethod("isin", list); + } + + /// + /// A boolean expression that is evaluated to true if the value of this expression + /// is contained by the evaluated values of the arguments. + /// + /// List of values to check the column against + /// Column object + public Column IsIn(params short[] list) + { + return ApplyMethod("isin", list); + } + + /// + /// A boolean expression that is evaluated to true if the value of this expression + /// is contained by the evaluated values of the arguments. + /// + /// List of values to check the column against + /// Column object + public Column IsIn(params float[] list) + { + return ApplyMethod("isin", list); + } + + /// + /// A boolean expression that is evaluated to true if the value of this expression + /// is contained by the evaluated values of the arguments. + /// + /// List of values to check the column against + /// Column object + public Column IsIn(params double[] list) + { + return ApplyMethod("isin", list); + } + + /// + /// A boolean expression that is evaluated to true if the value of this expression + /// is contained by the evaluated values of the arguments. + /// + /// List of values to check the column against + /// Column object + public Column IsIn(params decimal[] list) + { + return ApplyMethod("isin", list); + } + + /// + /// Gets the underlying Expression object of the . + /// + internal JvmObjectReference Expr() + { + return (JvmObjectReference)Reference.Invoke("expr"); + } + + // Equals() and GetHashCode() are required to be defined when operator==/!= + // are overloaded. + + /// + /// Checks if the given object is equal to this object. + /// + /// Object to compare to + /// True if the given object is equal to this object + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + return obj is Column other && Reference.Equals(other.Reference); + } + + /// + /// Calculates the hash code for this object. + /// + /// Hash code for this object + public override int GetHashCode() => Reference.GetHashCode(); + + /// + /// Invoke the toString method of the column instance + /// + /// Column name of this column + public override string ToString() => (string)Reference.Invoke("toString"); + + /// + /// Invokes a method under "org.apache.spark.sql.functions" with the given column. + /// + /// Column to apply function + /// Name of the function + /// New column after applying the function + private static Column ApplyFunction(Column column, string name) + { + return new Column( + (JvmObjectReference)column.Reference.Jvm.CallStaticJavaMethod( + "org.apache.spark.sql.functions", + name, + column)); + } + + /// + /// Invokes an operator (method name) with the current column. + /// + /// Operator to invoke + /// New column after applying the operator + private Column ApplyMethod(string op) + { + return new Column((JvmObjectReference)Reference.Invoke(op)); + } + + /// + /// Invokes an operator (method name) with the current column with other object. + /// + /// Operator to invoke + /// Object to apply the operator with + /// New column after applying the operator + private Column ApplyMethod(string op, object other) + { + return new Column((JvmObjectReference)Reference.Invoke(op, other)); + } + + /// + /// Invokes a method name with the current column with two other objects as parameters. + /// + /// Method to invoke + /// Object to apply the method with + /// Object to apply the method with + /// New column after applying the operator + private Column ApplyMethod(string op, object other1, object other2) + { + return new Column((JvmObjectReference)Reference.Invoke(op, other1, other2)); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/CustomPicklers.cs b/src/spark/Flowthru.Spark/Sql/CustomPicklers.cs new file mode 100644 index 00000000..fe4b33dc --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/CustomPicklers.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using Flowthru.Spark.Sql.Types; +using Razorvine.Pickle; + +namespace Flowthru.Spark.Sql +{ + /// + /// Custom pickler for Row objects. + /// + internal class RowPickler : IObjectPickler + { + public void pickle(object o, Stream outs, Pickler currentPickler) + { + currentPickler.save(((Row)o).Values); + } + } + + /// + /// Custom pickler for GenericRow objects. + /// + internal class GenericRowPickler : IObjectPickler + { + public void pickle(object o, Stream outs, Pickler currentPickler) + { + currentPickler.save(((GenericRow)o).Values); + } + } + + /// + /// Custom pickler for Date objects. + /// + internal class DatePickler : IObjectPickler + { + public void pickle(object o, Stream outs, Pickler currentPickler) + { + currentPickler.save(((Date)o).GetInterval()); + } + } + + /// + /// Custom pickler for Timestamp objects. + /// + internal class TimestampPickler : IObjectPickler + { + public void pickle(object o, Stream outs, Pickler currentPickler) + { + currentPickler.save(((Timestamp)o).GetIntervalInMicroseconds()); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/DataFrame.cs b/src/spark/Flowthru.Spark/Sql/DataFrame.cs new file mode 100644 index 00000000..f6f69a2f --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/DataFrame.cs @@ -0,0 +1,1132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Network; +using Flowthru.Spark.Sql.Streaming; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.Sql +{ + /// + /// A distributed collection of data organized into named columns. + /// + public sealed class DataFrame : IJvmObjectReferenceProvider + { + internal DataFrame(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Selects column based on the column name. + /// + /// Column name + /// Column object + public Column this[string columnName] + { + get + { + return WrapAsColumn(Reference.Invoke("col", columnName)); + } + } + + /// + /// Converts this strongly typed collection of data to generic `DataFrame`. + /// + /// + public DataFrame ToDF() => WrapAsDataFrame(Reference.Invoke("toDF")); + + /// + /// Converts this strongly typed collection of data to generic `DataFrame` + /// with columns renamed. + /// + /// Column names + /// DataFrame object + public DataFrame ToDF(params string[] colNames) => + WrapAsDataFrame(Reference.Invoke("toDF", (object)colNames)); + + /// + /// Returns the schema associated with this `DataFrame`. + /// + /// Schema associated with this data frame + public StructType Schema() => + new StructType((JvmObjectReference)Reference.Invoke("schema")); + + /// + /// Prints the schema to the console in a nice tree format. + /// + public void PrintSchema() => + Console.WriteLine( + (string)((JvmObjectReference)Reference.Invoke("schema")).Invoke("treeString")); + + /// + /// Prints the schema up to the given level to the console in a nice tree format. + /// + [Since(Versions.V3_0_0)] + public void PrintSchema(int level) + { + var schema = (JvmObjectReference)Reference.Invoke("schema"); + Console.WriteLine((string)schema.Invoke("treeString", level)); + } + + /// + /// Prints the plans (logical and physical) to the console for debugging purposes. + /// + /// prints only physical if set to false + public void Explain(bool extended = false) + { + var execution = (JvmObjectReference)Reference.Invoke("queryExecution"); + Console.WriteLine((string)execution.Invoke(extended ? "toString" : "simpleString")); + } + + /// + /// Prints the plans (logical and physical) with a format specified by a given explain + /// mode. + /// + /// + /// Specifies the expected output format of plans. + /// 1. `simple` Print only a physical plan. + /// 2. `extended`: Print both logical and physical plans. + /// 3. `codegen`: Print a physical plan and generated codes if they are available. + /// 4. `cost`: Print a logical plan and statistics if they are available. + /// 5. `formatted`: Split explain output into two sections: a physical plan outline and + /// node details. + /// + [Since(Versions.V3_0_0)] + public void Explain(string mode) + { + var execution = (JvmObjectReference)Reference.Invoke("queryExecution"); + var explainMode = (JvmObjectReference)Reference.Jvm.CallStaticJavaMethod( + "org.apache.spark.sql.execution.ExplainMode", + "fromString", + mode); + Console.WriteLine((string)execution.Invoke("explainString", explainMode)); + } + + /// + /// Returns all column names and their data types as an IEnumerable of Tuples. + /// + /// IEnumerable of Tuple of strings + public IEnumerable> DTypes() => + Schema().Fields.Select( + f => new Tuple(f.Name, f.DataType.SimpleString)); + + /// + /// Returns all column names. + /// + /// Column names + public IReadOnlyList Columns() => + Schema().Fields.Select(field => field.Name).ToArray(); + + /// + /// Returns true if the Collect() and Take() methods can be run locally without any + /// Spark executors. + /// + /// True if Collect() and Take() can be run locally + public bool IsLocal() => (bool)Reference.Invoke("isLocal"); + + /// + /// Returns true if this DataFrame is empty. + /// + /// True if empty + [Since(Versions.V2_4_0)] + public bool IsEmpty() => (bool)Reference.Invoke("isEmpty"); + + /// + /// Returns true if this `DataFrame` contains one or more sources that continuously + /// return data as it arrives. + /// + /// True if streaming DataFrame + public bool IsStreaming() => (bool)Reference.Invoke("isStreaming"); + + /// + /// Returns a checkpointed version of this `DataFrame`. + /// + /// + /// Checkpointing can be used to truncate the logical plan of this `DataFrame`, which is + /// especially useful in iterative algorithms where the plan may grow exponentially. + /// It will be saved to files inside the checkpoint directory set with + /// . + /// + /// Whether to checkpoint this `DataFrame` immediately + /// Checkpointed DataFrame + public DataFrame Checkpoint(bool eager = true) => + WrapAsDataFrame(Reference.Invoke("checkpoint", eager)); + + /// + /// Returns a locally checkpointed version of this `DataFrame`. + /// + /// + /// Checkpointing can be used to truncate the logical plan of this `DataFrame`, which is + /// especially useful in iterative algorithms where the plan may grow exponentially. + /// Local checkpoints are written to executor storage and despite potentially faster + /// they are unreliable and may compromise job completion. + /// + /// Whether to checkpoint this `DataFrame` immediately + /// DataFrame object + public DataFrame LocalCheckpoint(bool eager = true) => + WrapAsDataFrame(Reference.Invoke("localCheckpoint", eager)); + + /// + /// Defines an event time watermark for this DataFrame. A watermark tracks a point in time + /// before which we assume no more late data is going to arrive. + /// + /// + /// The name of the column that contains the event time of the row. + /// + /// + /// The minimum delay to wait to data to arrive late, relative to the latest record that + /// has been processed in the form of an interval (e.g. "1 minute" or "5 hours"). + /// + /// DataFrame object + public DataFrame WithWatermark(string eventTime, string delayThreshold) => + WrapAsDataFrame(Reference.Invoke("withWatermark", eventTime, delayThreshold)); + + /// + /// Displays rows of the `DataFrame` in tabular form. + /// + /// Number of rows to show + /// If set to more than 0, truncates strings to `truncate` + /// characters and all cells will be aligned right. + /// If set to true, prints output rows vertically + /// (one line per column value). + public void Show(int numRows = 20, int truncate = 20, bool vertical = false) => + Console.WriteLine(Reference.Invoke("showString", numRows, truncate, vertical)); + + /// + /// Returns a `DataFrameNaFunctions` for working with missing data. + /// + /// DataFrameNaFunctions object + public DataFrameNaFunctions Na() => + new DataFrameNaFunctions((JvmObjectReference)Reference.Invoke("na")); + + /// + ///Returns a `DataFrameStatFunctions` for working statistic functions support. + /// + /// DataFrameNaFunctions object + public DataFrameStatFunctions Stat() => + new DataFrameStatFunctions((JvmObjectReference)Reference.Invoke("stat")); + + /// + /// Returns the content of the DataFrame as a DataFrame of JSON strings. + /// + /// DataFrame object with JSON strings. + public DataFrame ToJSON() => + WrapAsDataFrame(Reference.Invoke("toJSON")); + + /// + /// Join with another `DataFrame`. + /// + /// + /// Behaves as an INNER JOIN and requires a subsequent join predicate. + /// + /// Right side of the join operator + /// DataFrame object + public DataFrame Join(DataFrame right) => + WrapAsDataFrame(Reference.Invoke("join", right)); + + /// + /// Inner equi-join with another `DataFrame` using the given column. + /// + /// Right side of the join operator + /// + /// Name of the column to join on. This column must exist on both sides. + /// + /// DataFrame object + public DataFrame Join(DataFrame right, string usingColumn) => + WrapAsDataFrame(Reference.Invoke("join", right, usingColumn)); + + /// + /// Equi-join with another `DataFrame` using the given columns. A cross join with + /// a predicate is specified as an inner join. If you would explicitly like to + /// perform a cross join use the `crossJoin` method. + /// + /// Right side of the join operator + /// Name of columns to join on + /// Type of join to perform. Default `inner`. Must be one of: + /// `inner`, `cross`, `outer`, `full`, `full_outer`, `left`, `left_outer`, `right`, + /// `right_outer`, `left_semi`, `left_anti` + /// DataFrame object + public DataFrame Join( + DataFrame right, + IEnumerable usingColumns, + string joinType = "inner") => + WrapAsDataFrame(Reference.Invoke("join", right, usingColumns, joinType)); + + /// + /// Join with another `DataFrame`, using the given join expression. + /// + /// Right side of the join operator + /// Join expression + /// Type of join to perform. Default `inner`. Must be one of: + /// `inner`, `cross`, `outer`, `full`, `full_outer`, `left`, `left_outer`, `right`, + /// `right_outer`, `left_semi`, `left_anti`. + /// + public DataFrame Join(DataFrame right, Column joinExpr, string joinType = "inner") => + WrapAsDataFrame(Reference.Invoke("join", right, joinExpr, joinType)); + + /// + /// Explicit Cartesian join with another `DataFrame`. + /// + /// + /// Cartesian joins are very expensive without an extra filter that can be pushed down. + /// + /// Right side of the join operator + /// DataFrame object + public DataFrame CrossJoin(DataFrame right) => + WrapAsDataFrame(Reference.Invoke("crossJoin", right)); + + /// + /// Returns a new `DataFrame` with each partition sorted by the given expressions. + /// + /// + /// This is the same operation as "SORT BY" in SQL (Hive QL). + /// + /// Column name to sort by + /// Additional column names to sort by + /// DataFrame object + public DataFrame SortWithinPartitions(string column, params string[] columns) => + WrapAsDataFrame(Reference.Invoke("sortWithinPartitions", column, columns)); + + /// + /// Returns a new `DataFrame` with each partition sorted by the given expressions. + /// + /// + /// This is the same operation as "SORT BY" in SQL (Hive QL). + /// + /// Column expressions to sort by + /// DataFrame object + public DataFrame SortWithinPartitions(params Column[] columns) => + WrapAsDataFrame(Reference.Invoke("sortWithinPartitions", (object)columns)); + + /// + /// Returns a new `DataFrame` sorted by the specified column, all in ascending order. + /// + /// Column name to sort by + /// Additional column names to sort by + /// DataFrame object + public DataFrame Sort(string column, params string[] columns) => + WrapAsDataFrame(Reference.Invoke("sort", column, columns)); + + /// + /// Returns a new `DataFrame` sorted by the given expressions. + /// + /// Column expressions to sort by + /// DataFrame object + public DataFrame Sort(params Column[] columns) => + WrapAsDataFrame(Reference.Invoke("sort", (object)columns)); + + /// + /// Returns a new Dataset sorted by the given expressions. + /// + /// + /// This is an alias of the Sort() function. + /// + /// Column name to sort by + /// Additional column names to sort by + /// + public DataFrame OrderBy(string column, params string[] columns) => + WrapAsDataFrame(Reference.Invoke("orderBy", column, columns)); + + /// + /// Returns a new Dataset sorted by the given expressions. + /// + /// + /// This is an alias of the Sort() function. + /// + /// Column expressions to sort by + /// DataFrame object + public DataFrame OrderBy(params Column[] columns) => + WrapAsDataFrame(Reference.Invoke("orderBy", (object)columns)); + + /// + /// Specifies some hint on the current `DataFrame`. + /// + /// + /// Due to the limitation of the type conversion between CLR and JVM, + /// the type of object in `parameters` should be the same. + /// + /// Name of the hint + /// Parameters of the hint + /// DataFrame object + public DataFrame Hint(string name, object[] parameters = null) + { + // If parameters are empty, create an empty int array so + // that the type conversion between CLR and JVM works. + return ((parameters == null) || (parameters.Length == 0)) ? + WrapAsDataFrame(Reference.Invoke("hint", name, new int[] { })) : + WrapAsDataFrame(Reference.Invoke("hint", name, parameters)); + } + + /// + /// Selects column based on the column name. + /// + /// Column name + /// Column object + public Column Col(string colName) => WrapAsColumn(Reference.Invoke("col", colName)); + + /// + /// Selects column based on the column name specified as a regex. + /// + /// Column name as a regex + /// Column object + public Column ColRegex(string colName) => + WrapAsColumn(Reference.Invoke("colRegex", colName)); + + /// + /// Returns a new `DataFrame` with an alias set. + /// + /// Alias name + /// Column object + public DataFrame As(string alias) => WrapAsDataFrame(Reference.Invoke("as", alias)); + + /// + /// Returns a new `DataFrame` with an alias set. Same as As(). + /// + /// Alias name + /// Column object + public DataFrame Alias(string alias) => WrapAsDataFrame(Reference.Invoke("alias", alias)); + + /// + /// Selects a set of column based expressions. + /// + /// Column expressions + /// DataFrame object + public DataFrame Select(params Column[] columns) => + WrapAsDataFrame(Reference.Invoke("select", (object)columns)); + + /// + /// Selects a set of columns. This is a variant of Select() that can only select + /// existing columns using column names (i.e. cannot construct expressions). + /// + /// Column name + /// Additional column names + /// DataFrame object + public DataFrame Select(string column, params string[] columns) => + WrapAsDataFrame(Reference.Invoke("select", column, columns)); + + /// + /// Selects a set of SQL expressions. This is a variant of Select() that + /// accepts SQL expressions. + /// + /// + /// DataFrame object + public DataFrame SelectExpr(params string[] expressions) => + WrapAsDataFrame(Reference.Invoke("selectExpr", (object)expressions)); + + /// + /// Filters rows using the given condition. + /// + /// Condition expression + /// DataFrame object + public DataFrame Filter(Column condition) => + WrapAsDataFrame(Reference.Invoke("filter", condition)); + + /// + /// Filters rows using the given SQL expression. + /// + /// SQL expression + /// DataFrame object + public DataFrame Filter(string conditionExpr) => + WrapAsDataFrame(Reference.Invoke("filter", conditionExpr)); + + /// + /// Filters rows using the given condition. This is an alias for Filter(). + /// + /// Condition expression + /// DataFrame object + public DataFrame Where(Column condition) => + WrapAsDataFrame(Reference.Invoke("where", condition)); + + /// + /// Filters rows using the given SQL expression. This is an alias for Filter(). + /// + /// SQL expression + /// DataFrame object + public DataFrame Where(string conditionExpr) => + WrapAsDataFrame(Reference.Invoke("where", conditionExpr)); + + /// + /// Groups the DataFrame using the specified columns, so we can run aggregation on them. + /// + /// Column expressions + /// RelationalGroupedDataset object + public RelationalGroupedDataset GroupBy(params Column[] columns) => + WrapAsGroupedDataset(Reference.Invoke("groupBy", (object)columns)); + + /// + /// Groups the DataFrame using the specified columns. + /// + /// Column name + /// Additional column names + /// RelationalGroupedDataset object + public RelationalGroupedDataset GroupBy(string column, params string[] columns) => + WrapAsGroupedDataset(Reference.Invoke("groupBy", column, columns)); + + /// + /// Create a multi-dimensional rollup for the current `DataFrame` using the + /// specified columns. + /// + /// Column expressions + /// RelationalGroupedDataset object + public RelationalGroupedDataset Rollup(params Column[] columns) => + WrapAsGroupedDataset(Reference.Invoke("rollup", (object)columns)); + + /// + /// Create a multi-dimensional rollup for the current `DataFrame` using the + /// specified columns. + /// + /// Column name + /// Additional column names + /// RelationalGroupedDataset object + public RelationalGroupedDataset Rollup(string column, params string[] columns) => + WrapAsGroupedDataset(Reference.Invoke("rollup", column, columns)); + + /// + /// Create a multi-dimensional cube for the current `DataFrame` using the + /// specified columns. + /// + /// Column expressions + /// RelationalGroupedDataset object + public RelationalGroupedDataset Cube(params Column[] columns) => + WrapAsGroupedDataset(Reference.Invoke("cube", (object)columns)); + + /// + /// Create a multi-dimensional cube for the current `DataFrame` using the + /// specified columns. + /// + /// Column name + /// Additional column names + /// RelationalGroupedDataset object + public RelationalGroupedDataset Cube(string column, params string[] columns) => + WrapAsGroupedDataset(Reference.Invoke("cube", column, columns)); + + /// + /// Aggregates on the entire `DataFrame` without groups. + /// + /// Column expression to aggregate + /// Additional column expressions + /// DataFrame object + public DataFrame Agg(Column expr, params Column[] exprs) => + WrapAsDataFrame(Reference.Invoke("agg", expr, exprs)); + + /// + /// Define (named) metrics to observe on the Dataset. This method returns an 'observed' + /// DataFrame that returns the same result as the input, with the following guarantees: + /// + /// 1. It will compute the defined aggregates(metrics) on all the data that is flowing + /// through the Dataset at that point. + /// 2. It will report the value of the defined aggregate columns as soon as we reach a + /// completion point.A completion point is either the end of a query(batch mode) or the end + /// of a streaming epoch. The value of the aggregates only reflects the data processed + /// since the previous completion point. + /// + /// Please note that continuous execution is currently not supported. + /// + /// Named metrics to observe + /// Defined aggregate to observe + /// Defined aggregates to observe + /// DataFrame object + [Since(Versions.V3_0_0)] + public DataFrame Observe(string name, Column expr, params Column[] exprs) => + WrapAsDataFrame(Reference.Invoke("observe", name, expr, exprs)); + + /// + /// Create a write configuration builder for v2 sources. + /// + /// Name of table to write to + /// DataFrameWriterV2 object + [Since(Versions.V3_0_0)] + public DataFrameWriterV2 WriteTo(string table) => + new DataFrameWriterV2((JvmObjectReference)Reference.Invoke("writeTo", table)); + + /// + /// Returns a new `DataFrame` by taking the first `number` rows. + /// + /// Number of rows to take + /// DataFrame object + public DataFrame Limit(int n) => WrapAsDataFrame(Reference.Invoke("limit", n)); + + /// + /// Returns a new `DataFrame` containing union of rows in this `DataFrame` + /// and another `DataFrame`. + /// + /// Other DataFrame + /// DataFrame object + public DataFrame Union(DataFrame other) => + WrapAsDataFrame(Reference.Invoke("union", other)); + + /// + /// Returns a new `DataFrame` containing union of rows in this `DataFrame` + /// and another `DataFrame`, resolving columns by name. + /// + /// Other DataFrame + /// DataFrame object + public DataFrame UnionByName(DataFrame other) => + WrapAsDataFrame(Reference.Invoke("unionByName", other)); + + /// + /// Returns a new containing union of rows in this + /// and another , resolving + /// columns by name. + /// + /// Other DataFrame + /// Allow missing columns + /// DataFrame object + [Since(Versions.V3_1_0)] + public DataFrame UnionByName(DataFrame other, bool allowMissingColumns) => + WrapAsDataFrame(Reference.Invoke("unionByName", other, allowMissingColumns)); + + /// + /// Returns a new `DataFrame` containing rows only in both this `DataFrame` + /// and another `DataFrame`. + /// + /// + /// This is equivalent to `INTERSECT` in SQL. + /// + /// Other DataFrame + /// DataFrame object + public DataFrame Intersect(DataFrame other) => + WrapAsDataFrame(Reference.Invoke("intersect", other)); + + /// + /// Returns a new `DataFrame` containing rows only in both this `DataFrame` + /// and another `DataFrame` while preserving the duplicates. + /// + /// + /// This is equivalent to `INTERSECT ALL` in SQL. + /// + /// Other DataFrame + /// DataFrame object + [Since(Versions.V2_4_0)] + public DataFrame IntersectAll(DataFrame other) => + WrapAsDataFrame(Reference.Invoke("intersectAll", other)); + + /// + /// Returns a new `DataFrame` containing rows in this `DataFrame` but + /// not in another `DataFrame`. + /// + /// + /// This is equivalent to `EXCEPT DISTINCT` in SQL. + /// + /// Other DataFrame + /// DataFrame object + public DataFrame Except(DataFrame other) => + WrapAsDataFrame(Reference.Invoke("except", other)); + + /// + /// Returns a new `DataFrame` containing rows in this `DataFrame` but + /// not in another `DataFrame` while preserving the duplicates. + /// + /// + /// This is equivalent to `EXCEPT ALL` in SQL. + /// + /// Other DataFrame + /// DataFrame object + [Since(Versions.V2_4_0)] + public DataFrame ExceptAll(DataFrame other) => + WrapAsDataFrame(Reference.Invoke("exceptAll", other)); + + /// + /// Returns a new `DataFrame` by sampling a fraction of rows (without replacement), + /// using a user-supplied seed. + /// + /// Fraction of rows + /// Sample with replacement or not + /// Optional random seed + /// DataFrame object + public DataFrame Sample( + double fraction, + bool withReplacement = false, + long? seed = null) => + WrapAsDataFrame( + seed.HasValue ? + Reference.Invoke("sample", withReplacement, fraction, seed.GetValueOrDefault()) : + Reference.Invoke("sample", withReplacement, fraction)); + + /// + /// Randomly splits this `DataFrame` with the provided weights. + /// + /// Weights for splits + /// Optional random seed + /// DataFrame object + public DataFrame[] RandomSplit(double[] weights, long? seed = null) + { + var dataFrames = (JvmObjectReference[])(seed.HasValue ? + Reference.Invoke("randomSplit", weights, seed.GetValueOrDefault()) : + Reference.Invoke("randomSplit", weights)); + + return dataFrames.Select(jvmObject => new DataFrame(jvmObject)).ToArray(); + } + + /// + /// Returns a new `DataFrame` by adding a column or replacing the existing column that + /// has the same name. + /// + /// Name of the new column + /// Column expression for the new column + /// DataFrame object + public DataFrame WithColumn(string colName, Column col) => + WrapAsDataFrame(Reference.Invoke("withColumn", colName, col)); + + /// + /// Returns a new Dataset with a column renamed. + /// This is a no-op if schema doesn't contain `existingName`. + /// + /// Existing column name + /// New column name to replace with + /// DataFrame object + public DataFrame WithColumnRenamed(string existingName, string newName) => + WrapAsDataFrame(Reference.Invoke("withColumnRenamed", existingName, newName)); + + /// + /// Returns a new `DataFrame` with columns dropped. + /// This is a no-op if schema doesn't contain column name(s). + /// + /// Name of columns to drop + /// DataFrame object + public DataFrame Drop(params string[] colNames) => + WrapAsDataFrame(Reference.Invoke("drop", (object)colNames)); + + /// + /// Returns a new `DataFrame` with a column dropped. + /// This is a no-op if the `DataFrame` doesn't have a column with an equivalent expression. + /// + /// Column expression + /// DataFrame object + public DataFrame Drop(Column col) => WrapAsDataFrame(Reference.Invoke("drop", col)); + + /// + /// Returns a new `DataFrame` that contains only the unique rows from this `DataFrame`. + /// This is an alias for Distinct(). + /// + /// + public DataFrame DropDuplicates() => WrapAsDataFrame(Reference.Invoke("dropDuplicates")); + + /// + /// Returns a new `DataFrame` with duplicate rows removed, considering only + /// the subset of columns. + /// + /// Column name + /// Additional column names + /// DataFrame object + public DataFrame DropDuplicates(string col, params string[] cols) => + WrapAsDataFrame(Reference.Invoke("dropDuplicates", col, cols)); + + /// + /// Computes basic statistics for numeric and string columns, including count, mean, + /// stddev, min, and max. If no columns are given, this function computes statistics for + /// all numerical or string columns. + /// + /// + /// This function is meant for exploratory data analysis, as we make no guarantee about + /// the backward compatibility of the schema of the resulting DataFrame. If you want to + /// programmatically compute summary statistics, use the `agg` function instead. + /// + /// Column names + /// DataFrame object + public DataFrame Describe(params string[] cols) => + WrapAsDataFrame(Reference.Invoke("describe", (object)cols)); + + /// + /// Computes specified statistics for numeric and string columns. + /// + /// + /// Available statistics are: + /// - count + /// - mean + /// - stddev + /// - min + /// - max + /// - arbitrary approximate percentiles specified as a percentage(e.g., 75%) + /// + /// If no statistics are given, this function computes count, mean, stddev, min, + /// approximate quartiles(percentiles at 25%, 50%, and 75%), and max. + /// + /// Statistics to compute + /// DataFrame object + public DataFrame Summary(params string[] statistics) => + WrapAsDataFrame(Reference.Invoke("summary", (object)statistics)); + + /// + /// Returns the first `n` rows. + /// + /// Number of rows + /// First `n` rows + public IEnumerable Head(int n) => Limit(n).Collect(); + + /// + /// Returns the first row. + /// + /// First row + public Row Head() => Limit(1).Collect().First(); + + /// + /// Returns the first row. Alis for Head(). + /// + /// First row + public Row First() => Head(); + + /// + /// Concise syntax for chaining custom transformations. + /// + /// + /// A function that takes and returns a + /// + /// Transformed DataFrame object. + public DataFrame Transform(Func func) => func(this); + + /// + /// Returns the first `n` rows in the `DataFrame`. + /// + /// Number of rows + /// First `n` rows + public IEnumerable Take(int n) => Head(n); + + /// + /// Returns the last `n` rows in the `DataFrame`. + /// + /// Number of rows + /// Last `n` rows + [Since(Versions.V3_0_0)] + public IEnumerable Tail(int n) + { + return GetRows("tailToPython", n); + } + + /// + /// Returns an array that contains all rows in this `DataFrame`. + /// + /// + /// This requires moving all the data into the application's driver process, and + /// doing so on a very large dataset can crash the driver process with OutOfMemoryError. + /// + /// Row objects + public IEnumerable Collect() + { + return GetRows("collectToPython"); + } + + /// + /// Returns an iterator that contains all of the rows in this `DataFrame`. + /// The iterator will consume as much memory as the largest partition in this `DataFrame`. + /// + /// Row objects + public IEnumerable ToLocalIterator() + { + Version version = SparkEnvironment.SparkVersion; + return version.Major switch + { + 2 => GetRows("toPythonIterator"), + 3 => ToLocalIterator(false), + 4 => ToLocalIterator(false), + _ => throw new NotSupportedException($"Spark {version} not supported.") + }; + } + + /// + /// Returns an iterator that contains all of the rows in this `DataFrame`. + /// The iterator will consume as much memory as the largest partition in this `DataFrame`. + /// With prefetch it may consume up to the memory of the 2 largest partitions. + /// + /// + /// If Spark should pre-fetch the next partition before it is needed. + /// + /// Row objects + [Since(Versions.V3_0_0)] + public IEnumerable ToLocalIterator(bool prefetchPartitions) + { + (int port, string secret, JvmObjectReference server) = + ParseConnectionInfo( + Reference.Invoke("toPythonIterator", prefetchPartitions), + true); + using ISocketWrapper socket = SocketFactory.CreateSocket(); + socket.Connect(IPAddress.Loopback, port, secret); + foreach (Row row in new RowCollector().Collect(socket, server)) + { + yield return row; + } + } + + /// + /// Returns the number of rows in the `DataFrame`. + /// + /// + public long Count() => (long)Reference.Invoke("count"); + + /// + /// Returns a new `DataFrame` that has exactly `numPartitions` partitions. + /// + /// Number of partitions + /// DataFrame object + public DataFrame Repartition(int numPartitions) => + WrapAsDataFrame(Reference.Invoke("repartition", numPartitions)); + + /// + /// Returns a new `DataFrame` partitioned by the given partitioning expressions into + /// `numPartitions`. The resulting `DataFrame` is hash partitioned. + /// + /// Number of partitions + /// Partitioning expressions + /// DataFrame object + public DataFrame Repartition(int numPartitions, params Column[] partitionExprs) => + WrapAsDataFrame(Reference.Invoke("repartition", numPartitions, partitionExprs)); + + /// + /// Returns a new `DataFrame` partitioned by the given partitioning expressions, using + /// `spark.sql.shuffle.partitions` as number of partitions. + /// + /// Partitioning expressions + /// DataFrame object + public DataFrame Repartition(params Column[] partitionExprs) => + WrapAsDataFrame(Reference.Invoke("repartition", (object)partitionExprs)); + + /// + /// Returns a new `DataFrame` partitioned by the given partitioning expressions into + /// `numPartitions`. The resulting `DataFrame` is range partitioned. + /// + /// Number of partitions + /// Partitioning expressions + /// DataFrame object + public DataFrame RepartitionByRange(int numPartitions, params Column[] partitionExprs) + { + if ((partitionExprs == null) || (partitionExprs.Length == 0)) + { + throw new ArgumentException("partitionExprs cannot be empty."); + } + + return WrapAsDataFrame( + Reference.Invoke("repartitionByRange", numPartitions, partitionExprs)); + } + + /// + /// Returns a new `DataFrame` partitioned by the given partitioning expressions, using + /// `spark.sql.shuffle.partitions` as number of partitions. + /// The resulting Dataset is range partitioned. + /// + /// Partitioning expressions + /// DataFrame object + public DataFrame RepartitionByRange(params Column[] partitionExprs) => + WrapAsDataFrame(Reference.Invoke("repartitionByRange", (object)partitionExprs)); + + /// + /// Returns a new `DataFrame` that has exactly `numPartitions` partitions, when the + /// fewer partitions are requested. If a larger number of partitions is requested, + /// it will stay at the current number of partitions. + /// + /// Number of partitions + /// DataFrame object + public DataFrame Coalesce(int numPartitions) => + WrapAsDataFrame(Reference.Invoke("coalesce", numPartitions)); + + /// + /// Returns a new Dataset that contains only the unique rows from this `DataFrame`. + /// This is an alias for DropDuplicates(). + /// + /// DataFrame object + public DataFrame Distinct() => WrapAsDataFrame(Reference.Invoke("distinct")); + + /// + /// Persist this with the default storage level MEMORY_AND_DISK. + /// + /// DataFrame object + public DataFrame Persist() => WrapAsDataFrame(Reference.Invoke("persist")); + + /// + /// Persist this with the given storage level. + /// + /// + /// to persist the to. + /// + /// DataFrame object + public DataFrame Persist(StorageLevel storageLevel) => + WrapAsDataFrame(Reference.Invoke("persist", storageLevel)); + + /// + /// Persist this with the default storage level MEMORY_AND_DISK. + /// + /// DataFrame object + public DataFrame Cache() => WrapAsDataFrame(Reference.Invoke("cache")); + + /// + /// Get the 's current . + /// + /// object + public StorageLevel StorageLevel() => + new StorageLevel((JvmObjectReference)Reference.Invoke("storageLevel")); + + /// + /// Mark the Dataset as non-persistent, and remove all blocks for it from memory and disk. + /// + /// + /// This will not un-persist any cached data that is built upon this `DataFrame`. + /// + /// + /// + public DataFrame Unpersist(bool blocking = false) => + WrapAsDataFrame(Reference.Invoke("unpersist", blocking)); + + /// + /// Creates a local temporary view using the given name. The lifetime of this + /// temporary view is tied to the SparkSession that created this `DataFrame`. + /// + /// Name of the view + public void CreateTempView(string viewName) + { + Reference.Invoke("createTempView", viewName); + } + + /// + /// Creates or replaces a local temporary view using the given name. The lifetime of this + /// temporary view is tied to the SparkSession that created this `DataFrame`. + /// + /// Name of the view + public void CreateOrReplaceTempView(string viewName) + { + Reference.Invoke("createOrReplaceTempView", viewName); + } + + /// + /// Creates a global temporary view using the given name. The lifetime of this + /// temporary view is tied to this Spark application. + /// + /// Name of the view + public void CreateGlobalTempView(string viewName) + { + Reference.Invoke("createGlobalTempView", viewName); + } + + /// + /// Creates or replaces a global temporary view using the given name. The lifetime of this + /// temporary view is tied to this Spark application. + /// + /// Name of the view + public void CreateOrReplaceGlobalTempView(string viewName) + { + Reference.Invoke("createOrReplaceGlobalTempView", viewName); + } + + /// + /// Interface for saving the content of the non-streaming Dataset out + /// into external storage. + /// + /// DataFrameWriter object + public DataFrameWriter Write() => + new DataFrameWriter((JvmObjectReference)Reference.Invoke("write")); + + /// + /// Interface for saving the content of the streaming Dataset out into external storage. + /// + /// DataStreamWriter object + public DataStreamWriter WriteStream() => + new DataStreamWriter((JvmObjectReference)Reference.Invoke("writeStream"), this); + + /// + /// Returns a best-effort snapshot of the files that compose this . + /// This method simply asks each constituent BaseRelation for its respective files and takes + /// the union of all results. Depending on the source relations, this may not find all input + /// files. Duplicates are removed. + /// + /// Files that compose this DataFrame + public IEnumerable InputFiles() => (string[])Reference.Invoke("inputFiles"); + + /// + /// Returns `true` when the logical query plans inside both s are + /// equal and therefore return same results. + /// + /// + /// The equality comparison here is simplified by tolerating the cosmetic differences + /// such as attribute names. + /// + /// This API can compare both s very fast but can still return `false` + /// on the that return the same results, for instance, from different + /// plans. Such false negative semantic can be useful when caching as an example. + /// + /// Other DataFrame + /// + /// `true` when the logical query plans inside both s are + /// equal and therefore return same results. + /// + [Since(Versions.V3_1_0)] + public bool SameSemantics(DataFrame other) => + (bool)Reference.Invoke("sameSemantics", other); + + /// + /// Returns a hash code of the logical query plan against this . + /// + /// + /// Unlike the standard hash code, the hash is calculated against the query plan + /// simplified by tolerating the cosmetic differences such as attribute names. + /// + /// Hash code of the logical query plan + [Since(Versions.V3_1_0)] + public int SemanticHash() => + (int)Reference.Invoke("semanticHash"); + + /// + /// Returns row objects based on the function (either "toPythonIterator", + /// "collectToPython", or "tailToPython"). + /// + /// String name of function to call + /// Arguments to the function + /// IEnumerable of Rows from Spark + private IEnumerable GetRows(string funcName, params object[] args) + { + (int port, string secret, _) = GetConnectionInfo(funcName, args); + using ISocketWrapper socket = SocketFactory.CreateSocket(); + socket.Connect(IPAddress.Loopback, port, secret); + foreach (Row row in new RowCollector().Collect(socket)) + { + yield return row; + } + } + + /// + /// Returns a tuple of port number and secret string which are + /// used for connecting with Spark to receive rows for this `DataFrame`. + /// + /// A tuple of port number, secret string, and JVM socket auth server. + private (int, string, JvmObjectReference) GetConnectionInfo( + string funcName, + params object[] args) + { + object result = Reference.Invoke(funcName, args); + Version version = SparkEnvironment.SparkVersion; + return (version.Major, version.Minor) switch + { + // PythonFunction.serveIterator() returns a pair where the first is a port + // number and the second is the secret string to use for the authentication. + (2, 4) => ParseConnectionInfo(result, false), + (3, _) => ParseConnectionInfo(result, false), + (4, _) => ParseConnectionInfo(result, false), + _ => throw new NotSupportedException($"Spark {version} not supported.") + }; + } + + private (int, string, JvmObjectReference) ParseConnectionInfo( + object info, + bool parseServer) + { + var infos = (JvmObjectReference[])info; + return ((int)infos[0].Invoke("intValue"), + (string)infos[1].Invoke("toString"), + parseServer ? infos[2] : null); + } + + private DataFrame WrapAsDataFrame(object obj) => new DataFrame((JvmObjectReference)obj); + + private Column WrapAsColumn(object obj) => new Column((JvmObjectReference)obj); + + private RelationalGroupedDataset WrapAsGroupedDataset(object obj) => + new RelationalGroupedDataset((JvmObjectReference)obj, this); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/DataFrameFunctions.cs b/src/spark/Flowthru.Spark/Sql/DataFrameFunctions.cs new file mode 100644 index 00000000..f755741b --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/DataFrameFunctions.cs @@ -0,0 +1,274 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Data.Analysis; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Sql +{ + /// + /// Functions available for a managed DataFrame. + /// + public static class DataFrameFunctions + { + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf(Func udf) + where T : DataFrameColumn + where TResult : DataFrameColumn + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + DataFrameUdfUtils.CreateVectorUdfWrapper(udf)).Apply1; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf(Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where TResult : DataFrameColumn + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + DataFrameUdfUtils.CreateVectorUdfWrapper(udf)).Apply2; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where TResult : DataFrameColumn + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + DataFrameUdfUtils.CreateVectorUdfWrapper(udf)).Apply3; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where TResult : DataFrameColumn + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + DataFrameUdfUtils.CreateVectorUdfWrapper(udf)).Apply4; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where TResult : DataFrameColumn + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + DataFrameUdfUtils.CreateVectorUdfWrapper(udf)).Apply5; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where TResult : DataFrameColumn + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + DataFrameUdfUtils.CreateVectorUdfWrapper(udf)).Apply6; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where TResult : DataFrameColumn + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + DataFrameUdfUtils.CreateVectorUdfWrapper(udf)).Apply7; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where TResult : DataFrameColumn + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + DataFrameUdfUtils.CreateVectorUdfWrapper(udf)).Apply8; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where T9 : DataFrameColumn + where TResult : DataFrameColumn + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + DataFrameUdfUtils.CreateVectorUdfWrapper(udf)).Apply9; + } + + /// Creates a Vector UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the type of the tenth argument to the UDF. + /// Specifies the return type of the UDF. + /// The Vector UDF function implementation. + /// + /// A delegate that returns a for the result of the Vector UDF. + /// + public static Func VectorUdf( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where T9 : DataFrameColumn + where T10 : DataFrameColumn + where TResult : DataFrameColumn + { + return Functions.CreateVectorUdf( + udf.Method.ToString(), + DataFrameUdfUtils.CreateVectorUdfWrapper(udf)).Apply10; + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/DataFrameGroupedMapUdfWrapper.cs b/src/spark/Flowthru.Spark/Sql/DataFrameGroupedMapUdfWrapper.cs new file mode 100644 index 00000000..d98c25a8 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/DataFrameGroupedMapUdfWrapper.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Apache.Arrow; +using FxDataFrame = Microsoft.Data.Analysis.DataFrame; + +namespace Flowthru.Spark.Sql +{ + /// + /// Wraps the given Func object, which represents a Grouped Map UDF. + /// + /// + /// UDF serialization requires a "wrapper" object in order to serialize/deserialize. + /// + [UdfWrapper] + internal sealed class DataFrameGroupedMapUdfWrapper + { + private readonly Func _func; + + internal DataFrameGroupedMapUdfWrapper(Func func) + { + _func = func; + } + + internal FxDataFrame Execute(FxDataFrame input) + { + return _func(input); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/DataFrameNaFunctions.cs b/src/spark/Flowthru.Spark/Sql/DataFrameNaFunctions.cs new file mode 100644 index 00000000..100072fe --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/DataFrameNaFunctions.cs @@ -0,0 +1,286 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql +{ + /// + /// Provides functionalities for working with missing data in . + /// + public sealed class DataFrameNaFunctions : IJvmObjectReferenceProvider + { + internal DataFrameNaFunctions(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Returns a new `DataFrame` that drops rows containing any null or NaN values. + /// + /// DataFrame object + public DataFrame Drop() => WrapAsDataFrame(Reference.Invoke("drop")); + + /// + /// Returns a new `DataFrame` that drops rows containing null or NaN values. + /// + /// + /// If `how` is "any", then drop rows containing any null or NaN values. + /// If `how` is "all", then drop rows only if every column is null or NaN for that row. + /// + /// Determines the behavior of dropping rows + /// DataFrame object + public DataFrame Drop(string how) => WrapAsDataFrame(Reference.Invoke("drop", how)); + + /// + /// Returns a new `DataFrame` that drops rows containing any null or NaN values + /// in the specified columns. + /// + /// Column names + /// DataFrame object + public DataFrame Drop(IEnumerable columnNames) => + WrapAsDataFrame(Reference.Invoke("drop", columnNames)); + + /// + /// Returns a new `DataFrame` that drops rows containing any null or NaN values + /// in the specified columns. + /// + /// + /// If `how` is "any", then drop rows containing any null or NaN values. + /// If `how` is "all", then drop rows only if every column is null or NaN for that row. + /// + /// Determines the behavior of dropping rows + /// Column names + /// DataFrame object + public DataFrame Drop(string how, IEnumerable columnNames) => + WrapAsDataFrame(Reference.Invoke("drop", how, columnNames)); + + /// + /// Returns a new `DataFrame` that drops rows containing less than `minNonNulls` + /// non-null and non-NaN values. + /// + /// + /// DataFrame object + public DataFrame Drop(int minNonNulls) => + WrapAsDataFrame(Reference.Invoke("drop", minNonNulls)); + + /// + /// Returns a new `DataFrame` that drops rows containing less than `minNonNulls` + /// non-null and non-NaN values in the specified columns. + /// + /// + /// Column names + /// DataFrame object + public DataFrame Drop(int minNonNulls, IEnumerable columnNames) => + WrapAsDataFrame(Reference.Invoke("drop", minNonNulls, columnNames)); + + /// + /// Returns a new `DataFrame` that replaces null or NaN values in numeric columns + /// with `value`. + /// + /// Value to replace with + /// DataFrame object + public DataFrame Fill(long value) => WrapAsDataFrame(Reference.Invoke("fill", value)); + + /// + /// Returns a new `DataFrame` that replaces null or NaN values in numeric columns + /// with `value`. + /// + /// Value to replace with + /// DataFrame object + public DataFrame Fill(double value) => WrapAsDataFrame(Reference.Invoke("fill", value)); + + /// + /// Returns a new `DataFrame` that replaces null or NaN values in numeric columns + /// with `value`. + /// + /// Value to replace with + /// DataFrame object + public DataFrame Fill(string value) => WrapAsDataFrame(Reference.Invoke("fill", value)); + + /// + /// Returns a new `DataFrame` that replaces null or NaN values in specified numeric + /// columns. If a specified column is not a numeric column, it is ignored. + /// + /// Value to replace with + /// Column names + /// DataFrame object + public DataFrame Fill(long value, IEnumerable columnNames) => + WrapAsDataFrame(Reference.Invoke("fill", value, columnNames)); + + /// + /// Returns a new `DataFrame` that replaces null or NaN values in specified numeric + /// columns. If a specified column is not a numeric column, it is ignored. + /// + /// Value to replace with + /// Column names + /// DataFrame object + public DataFrame Fill(double value, IEnumerable columnNames) => + WrapAsDataFrame(Reference.Invoke("fill", value, columnNames)); + + /// + /// Returns a new `DataFrame` that replaces null or NaN values in specified string + /// columns. If a specified column is not a string column, it is ignored. + /// + /// Value to replace with + /// Column names + /// DataFrame object + public DataFrame Fill(string value, IEnumerable columnNames) => + WrapAsDataFrame(Reference.Invoke("fill", value, columnNames)); + + /// + /// Returns a new `DataFrame` that replaces null values in boolean columns with `value`. + /// + /// Value to replace with + /// DataFrame object + public DataFrame Fill(bool value) => WrapAsDataFrame(Reference.Invoke("fill", value)); + + /// + /// Returns a new `DataFrame` that replaces null or NaN values in specified boolean + /// columns. If a specified column is not a boolean column, it is ignored. + /// + /// Value to replace with + /// Column names + /// DataFrame object + public DataFrame Fill(bool value, IEnumerable columnNames) => + WrapAsDataFrame(Reference.Invoke("fill", value, columnNames)); + + /// + /// Returns a new `DataFrame` that replaces null values. + /// + /// + /// The key of the map is the column name, and the value of the map is the + /// replacement value. + /// + /// Values to replace null values + /// DataFrame object + public DataFrame Fill(IDictionary valueMap) => + WrapAsDataFrame(Reference.Invoke("fill", valueMap)); + + /// + /// Returns a new `DataFrame` that replaces null values. + /// + /// + /// The key of the map is the column name, and the value of the map is the + /// replacement value. + /// + /// Values to replace null values + /// DataFrame object + public DataFrame Fill(IDictionary valueMap) => + WrapAsDataFrame(Reference.Invoke("fill", valueMap)); + + /// + /// Returns a new `DataFrame` that replaces null values. + /// + /// + /// The key of the map is the column name, and the value of the map is the + /// replacement value. + /// + /// Values to replace null values + /// DataFrame object + public DataFrame Fill(IDictionary valueMap) => + WrapAsDataFrame(Reference.Invoke("fill", valueMap)); + + /// + /// Returns a new `DataFrame` that replaces null values. + /// + /// + /// The key of the map is the column name, and the value of the map is the + /// replacement value. + /// + /// Values to replace null values + /// DataFrame object + public DataFrame Fill(IDictionary valueMap) => + WrapAsDataFrame(Reference.Invoke("fill", valueMap)); + + /// + /// Returns a new `DataFrame` that replaces null values. + /// + /// + /// The key of the map is the column name, and the value of the map is the + /// replacement value. + /// + /// Values to replace null values + /// DataFrame object + public DataFrame Fill(IDictionary valueMap) => + WrapAsDataFrame(Reference.Invoke("fill", valueMap)); + + /// + /// Replaces values matching keys in `replacement` map with the corresponding values. + /// + /// + /// Name of the column to apply the value replacement. If `col` is "*", replacement + /// is applied on all string, numeric or boolean columns. + /// + /// Map that stores the replacement values + /// DataFrame object + public DataFrame Replace(string columnName, IDictionary replacement) => + WrapAsDataFrame(Reference.Invoke("replace", columnName, replacement)); + + /// + /// Replaces values matching keys in `replacement` map with the corresponding values. + /// + /// + /// Name of the column to apply the value replacement. If `col` is "*", replacement + /// is applied on all string, numeric or boolean columns. + /// + /// Map that stores the replacement values + /// DataFrame object + public DataFrame Replace(string columnName, IDictionary replacement) => + WrapAsDataFrame(Reference.Invoke("replace", columnName, replacement)); + + /// + /// Replaces values matching keys in `replacement` map with the corresponding values. + /// + /// + /// Name of the column to apply the value replacement. If `col` is "*", replacement + /// is applied on all string, numeric or boolean columns. + /// + /// Map that stores the replacement values + /// DataFrame object + public DataFrame Replace(string columnName, IDictionary replacement) => + WrapAsDataFrame(Reference.Invoke("replace", columnName, replacement)); + + /// + /// Replaces values matching keys in `replacement` map with the corresponding values. + /// + /// + /// Name of the column to apply the value replacement. If `col` is "*", replacement + /// is applied on all string, numeric or boolean columns. + /// + /// Map that stores the replacement values + /// DataFrame object + public DataFrame Replace( + IEnumerable columnNames, + IDictionary replacement) => + WrapAsDataFrame(Reference.Invoke("replace", columnNames, replacement)); + + /// + /// Replaces values matching keys in `replacement` map with the corresponding values. + /// + /// list of columns to apply the value replacement. + /// Map that stores the replacement values + /// DataFrame object + public DataFrame Replace( + IEnumerable columnNames, + IDictionary replacement) => + WrapAsDataFrame(Reference.Invoke("replace", columnNames, replacement)); + + /// + /// Replaces values matching keys in `replacement` map with the corresponding values. + /// + /// list of columns to apply the value replacement. + /// Map that stores the replacement values + /// DataFrame object + public DataFrame Replace( + IEnumerable columnNames, + IDictionary replacement) => + WrapAsDataFrame(Reference.Invoke("replace", columnNames, replacement)); + + private DataFrame WrapAsDataFrame(object obj) => new DataFrame((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/DataFrameReader.cs b/src/spark/Flowthru.Spark/Sql/DataFrameReader.cs new file mode 100644 index 00000000..7c070fd3 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/DataFrameReader.cs @@ -0,0 +1,301 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Flowthru.Spark.Interop.Internal.Java.Util; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.Sql +{ + /// + /// DataFrameReader provides functionality to load a + /// from external storage systems (e.g. file systems, key-value stores, etc). + /// + public sealed class DataFrameReader : IJvmObjectReferenceProvider + { + internal DataFrameReader(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Specifies the input data source format. + /// + /// Name of the data source + /// This DataFrameReader object + public DataFrameReader Format(string source) + { + Reference.Invoke("format", source); + return this; + } + + /// + /// Specifies the schema by using . + /// + /// + /// Some data sources (e.g. JSON) can infer the input schema automatically + /// from data. By specifying the schema here, the underlying data source can + /// skip the schema inference step, and thus speed up data loading. + /// + /// The input schema + /// This DataFrameReader object + public DataFrameReader Schema(StructType schema) + { + Reference.Invoke("schema", DataType.FromJson(Reference.Jvm, schema.Json)); + return this; + } + + /// + /// Specifies the schema by using the given DDL-formatted string. + /// + /// + /// Some data sources (e.g. JSON) can infer the input schema automatically + /// from data. By specifying the schema here, the underlying data source can + /// skip the schema inference step, and thus speed up data loading. + /// + /// DDL-formatted string + /// This DataFrameReader object + public DataFrameReader Schema(string schemaString) + { + Reference.Invoke("schema", schemaString); + return this; + } + + /// + /// Adds an input option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataFrameReader object + public DataFrameReader Option(string key, string value) + { + return OptionInternal(key, value); + } + + /// + /// Adds an input option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataFrameReader object + public DataFrameReader Option(string key, bool value) + { + return OptionInternal(key, value); + } + + /// + /// Adds an input option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataFrameReader object + public DataFrameReader Option(string key, long value) + { + return OptionInternal(key, value); + } + + /// + /// Adds an input option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataFrameReader object + public DataFrameReader Option(string key, double value) + { + return OptionInternal(key, value); + } + + /// + /// Adds input options for the underlying data source. + /// + /// Key/value options + /// This DataFrameReader object + public DataFrameReader Options(Dictionary options) + { + Reference.Invoke("options", options); + return this; + } + + /// + /// Loads input in as a DataFrame, for data sources that don't require a path + /// (e.g. external key-value stores). + /// + /// DataFrame object + public DataFrame Load() => new DataFrame((JvmObjectReference)Reference.Invoke("load")); + + /// + /// Loads input in as a DataFrame, for data sources that require a path + /// (e.g. data backed by a local or distributed file system). + /// + /// Input path + /// DataFrame object + public DataFrame Load(string path) => + new DataFrame((JvmObjectReference)Reference.Invoke("load", path)); + + /// + /// Loads input in as a DataFrame from the given paths. + /// + /// + /// Paths can be empty if data sources don't require a path (e.g. external + /// key-value stores). + /// + /// Input paths + /// DataFrame object + public DataFrame Load(params string[] paths) => + new DataFrame((JvmObjectReference)Reference.Invoke("load", (object)paths)); + + /// + /// Construct a DataFrame representing the database table accessible via JDBC URL + /// url named table and connection properties. + /// + /// JDBC database url of the form "jdbc:subprotocol:subname" + /// Name of the table in the external database + /// JDBC database connection arguments + /// DataFrame representing the database table accessible via JDBC + public DataFrame Jdbc(string url, string table, Dictionary properties) => + new DataFrame((JvmObjectReference)Reference.Invoke( + "jdbc", + url, + table, + new Properties(Reference.Jvm, properties))); + + /// + /// Construct a DataFrame representing the database table accessible via JDBC URL + /// url named table. Partitions of the table will be retrieved in parallel based + /// on the parameters passed to this function. + /// + /// JDBC database url of the form "jdbc:subprotocol:subname" + /// Name of the table in the external database + /// The name of a column of integral type that will be used + /// for partitioning + /// The minimum value of columnName used to decide partition + /// stride. + /// The maximum value of columnName used to decide partition + /// stride + /// + /// The number of partitions. This, along with lowerBound (inclusive), + /// upperBound(exclusive), form partition strides for generated WHERE + /// clause expressions used to split the column columnName evenly.When + /// the input is less than 1, the number is set to 1. + /// + /// JDBC database connection arguments + /// DataFrame representing the database table accessible via JDBC + public DataFrame Jdbc( + string url, + string table, + string columnName, + long lowerBound, + long upperBound, + int numPartitions, + Dictionary properties) => + new DataFrame((JvmObjectReference)Reference.Invoke( + "jdbc", + url, + table, + columnName, + lowerBound, + upperBound, + numPartitions, + new Properties(Reference.Jvm, properties))); + + /// + /// Construct a DataFrame representing the database table accessible via JDBC URL + /// url named table and connection properties. The predicates parameter gives a list + /// expressions suitable for inclusion in WHERE clauses; each one defines one partition + /// of the DataFrame. + /// + /// JDBC database url of the form "jdbc:subprotocol:subname" + /// Name of the table in the external database + /// Condition in the WHERE clause for each partition + /// JDBC database connection arguments + /// DataFrame representing the database table accessible via JDBC + public DataFrame Jdbc( + string url, + string table, + IEnumerable predicates, + Dictionary properties) => + new DataFrame((JvmObjectReference)Reference.Invoke( + "jdbc", + url, + table, + predicates, + new Properties(Reference.Jvm, properties))); + + /// + /// Loads a JSON file (one object per line) and returns the result as a DataFrame. + /// + /// Input paths + /// DataFrame object + public DataFrame Json(params string[] paths) => LoadSource("json", paths); + + /// + /// Loads CSV files and returns the result as a DataFrame. + /// + /// Input paths + /// DataFrame object + public DataFrame Csv(params string[] paths) => LoadSource("csv", paths); + + /// + /// Loads a Parquet file, returning the result as a DataFrame. + /// + /// Input paths + /// DataFrame object + public DataFrame Parquet(params string[] paths) => LoadSource("parquet", paths); + + /// + /// Loads an ORC file and returns the result as a DataFrame. + /// + /// Input paths + /// DataFrame object + public DataFrame Orc(params string[] paths) => LoadSource("orc", paths); + + /// + /// Returns the specified table as a DataFrame. + /// + /// Name of the table to read + /// DataFrame object + public DataFrame Table(string tableName) => + new DataFrame((JvmObjectReference)Reference.Invoke("table", tableName)); + + /// + /// Loads text files and returns a DataFrame whose schema starts with a string column + /// named "value", and followed by partitioned columns if there are any. + /// + /// Input paths + /// DataFrame object + public DataFrame Text(params string[] paths) => LoadSource("text", paths); + + /// + /// Helper function to add given key/value pair as a new option. + /// + /// Name of the option + /// Value of the option + /// This DataFrameReader object + private DataFrameReader OptionInternal(string key, object value) + { + Reference.Invoke("option", key, value); + return this; + } + + /// + /// Helper function to create a DataFrame with the given source and paths. + /// + /// Name of the data source + /// Input paths + /// A DataFrame object + private DataFrame LoadSource(string source, params string[] paths) + { + if (paths.Length == 0) + { + throw new ArgumentException($"paths cannot be empty for source: {source}"); + } + + return new DataFrame((JvmObjectReference)Reference.Invoke(source, (object)paths)); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/DataFrameStatFunctions.cs b/src/spark/Flowthru.Spark/Sql/DataFrameStatFunctions.cs new file mode 100644 index 00000000..1e3c8293 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/DataFrameStatFunctions.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql +{ + /// + /// Provides statistic functions for . + /// + public sealed class DataFrameStatFunctions : IJvmObjectReferenceProvider + { + internal DataFrameStatFunctions(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Calculates the approximate quantiles of a numerical column of a DataFrame. + /// + /// + /// This method implements a variation of the Greenwald-Khanna algorithm + /// (with some speed optimizations). + /// + /// Column name + /// A list of quantile probabilities + /// + /// The relative target precision to achieve (greater than or equal to 0) + /// + /// The approximate quantiles at the given probabilities + public double[] ApproxQuantile( + string columnName, + IEnumerable probabilities, + double relativeError) => + (double[])Reference.Invoke( + "approxQuantile", columnName, probabilities, relativeError); + + /// + /// Calculate the sample covariance of two numerical columns of a DataFrame. + /// + /// First column name + /// Second column name + /// The covariance of the two columns + public double Cov(string colName1, string colName2) => + (double)Reference.Invoke("cov", colName1, colName2); + + /// + /// Calculates the correlation of two columns of a DataFrame. + /// + /// + /// Currently only the Pearson Correlation Coefficient is supported. + /// + /// First column name + /// Second column name + /// Method name for calculating correlation + /// The Pearson Correlation Coefficient + public double Corr(string colName1, string colName2, string method) => + (double)Reference.Invoke("corr", colName1, colName2, method); + + /// + /// Calculates the Pearson Correlation Coefficient of two columns of a DataFrame. + /// + /// First column name + /// Second column name + /// The Pearson Correlation Coefficient + public double Corr(string colName1, string colName2) => + (double)Reference.Invoke("corr", colName1, colName2); + + /// + /// Computes a pair-wise frequency table of the given columns, also known as + /// a contingency table. + /// + /// First column name + /// Second column name + /// DataFrame object + public DataFrame Crosstab(string colName1, string colName2) => + WrapAsDataFrame(Reference.Invoke("crosstab", colName1, colName2)); + + /// + /// Finding frequent items for columns, possibly with false positives. + /// + /// Column names + /// + /// The minimum frequency for an item to be considered frequent. + /// Should be greater than 1e-4. + /// + /// DataFrame object + public DataFrame FreqItems(IEnumerable columnNames, double support) => + WrapAsDataFrame(Reference.Invoke("freqItems", columnNames, support)); + + /// + /// Finding frequent items for columns, possibly with false positives with + /// a default support of 1%. + /// + /// Column names + /// DataFrame object + public DataFrame FreqItems(IEnumerable columnNames) => + WrapAsDataFrame(Reference.Invoke("freqItems", columnNames)); + + /// + /// Returns a stratified sample without replacement based on the fraction given + /// on each stratum. + /// + /// Stratum type + /// Column name that defines strata + /// + /// Sampling fraction for each stratum. If a stratum is not specified, we treat + /// its fraction as zero. + /// + /// Random seed + /// DataFrame object + public DataFrame SampleBy( + string columnName, + IDictionary fractions, + long seed) => + WrapAsDataFrame(Reference.Invoke("sampleBy", columnName, fractions, seed)); + + /// + /// Returns a stratified sample without replacement based on the fraction given + /// on each stratum. + /// + /// Stratum type + /// Column that defines strata + /// + /// Sampling fraction for each stratum. If a stratum is not specified, we treat + /// its fraction as zero. + /// + /// Random seed + /// DataFrame object + [Since(Versions.V3_0_0)] + public DataFrame SampleBy(Column column, IDictionary fractions, long seed) => + WrapAsDataFrame(Reference.Invoke("sampleBy", column, fractions, seed)); + + private DataFrame WrapAsDataFrame(object obj) => new DataFrame((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/DataFrameUdfRegistrationExtensions.cs b/src/spark/Flowthru.Spark/Sql/DataFrameUdfRegistrationExtensions.cs new file mode 100644 index 00000000..dcdf363b --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/DataFrameUdfRegistrationExtensions.cs @@ -0,0 +1,271 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Data.Analysis; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Sql +{ + /// + /// Extension methods for . + /// + public static class DataFrameUdfRegistrationExtensions + { + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T : DataFrameColumn + where TResult : DataFrameColumn + { + RegisterVector(udf, name, DataFrameUdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where TResult : DataFrameColumn + { + RegisterVector(udf, name, DataFrameUdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where TResult : DataFrameColumn + { + RegisterVector(udf, name, DataFrameUdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where TResult : DataFrameColumn + { + RegisterVector(udf, name, DataFrameUdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where TResult : DataFrameColumn + { + RegisterVector(udf, name, DataFrameUdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where TResult : DataFrameColumn + { + RegisterVector(udf, name, DataFrameUdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where TResult : DataFrameColumn + { + RegisterVector(udf, name, DataFrameUdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where TResult : DataFrameColumn + { + RegisterVector(udf, name, DataFrameUdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where T9 : DataFrameColumn + where TResult : DataFrameColumn + { + RegisterVector(udf, name, DataFrameUdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the type of the tenth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where T9 : DataFrameColumn + where T10 : DataFrameColumn + where TResult : DataFrameColumn + { + RegisterVector(udf, name, DataFrameUdfUtils.CreateVectorUdfWrapper(f)); + } + + private static void RegisterVector(UdfRegistration udf, string name, Delegate func) + { + udf.Register(name, func, UdfUtils.PythonEvalType.SQL_SCALAR_PANDAS_UDF); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/DataFrameUdfWrapper.cs b/src/spark/Flowthru.Spark/Sql/DataFrameUdfWrapper.cs new file mode 100644 index 00000000..d7e387a2 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/DataFrameUdfWrapper.cs @@ -0,0 +1,473 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Apache.Arrow; +using Microsoft.Data.Analysis; +using static Flowthru.Spark.Sql.ArrowArrayHelpers; + +namespace Flowthru.Spark.Sql +{ + /// + /// An abstract class to detect DataFrameUdfWrapper derivatives at runtime + /// + internal abstract class DataFrameUdfWrapper + { + + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class DataFrameUdfWrapper : DataFrameUdfWrapper + where T : DataFrameColumn + where TResult : DataFrameColumn + { + private readonly Func _func; + + internal DataFrameUdfWrapper(Func func) + { + _func = func; + } + + internal DataFrameColumn Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + long length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T)columns[argOffsets[0]]); + } + + return CreateEmptyColumn(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class DataFrameUdfWrapper : DataFrameUdfWrapper + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where TResult : DataFrameColumn + { + private readonly Func _func; + + internal DataFrameUdfWrapper(Func func) + { + _func = func; + } + + internal DataFrameColumn Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + long length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]]); + } + return CreateEmptyColumn(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class DataFrameUdfWrapper : DataFrameUdfWrapper + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where TResult : DataFrameColumn + { + private readonly Func _func; + + internal DataFrameUdfWrapper(Func func) + { + _func = func; + } + + internal DataFrameColumn Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + long length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]]); + } + + return CreateEmptyColumn(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class DataFrameUdfWrapper : DataFrameUdfWrapper + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where TResult : DataFrameColumn + { + private readonly Func _func; + + internal DataFrameUdfWrapper(Func func) + { + _func = func; + } + + internal DataFrameColumn Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + long length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]]); + } + + return CreateEmptyColumn(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class DataFrameUdfWrapper : DataFrameUdfWrapper + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where TResult : DataFrameColumn + { + private readonly Func _func; + + internal DataFrameUdfWrapper(Func func) + { + _func = func; + } + + internal DataFrameColumn Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + long length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]]); + } + + return CreateEmptyColumn(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class DataFrameUdfWrapper : DataFrameUdfWrapper + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where TResult : DataFrameColumn + { + private readonly Func _func; + + internal DataFrameUdfWrapper(Func func) + { + _func = func; + } + + internal DataFrameColumn Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + long length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]], + (T6)columns[argOffsets[5]]); + } + + return CreateEmptyColumn(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class DataFrameUdfWrapper : DataFrameUdfWrapper + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where TResult : DataFrameColumn + { + private readonly Func _func; + + internal DataFrameUdfWrapper(Func func) + { + _func = func; + } + + internal DataFrameColumn Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + long length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]], + (T6)columns[argOffsets[5]], + (T7)columns[argOffsets[6]]); + } + + return CreateEmptyColumn(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class DataFrameUdfWrapper : DataFrameUdfWrapper + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where TResult : DataFrameColumn + { + private readonly Func _func; + + internal DataFrameUdfWrapper(Func func) + { + _func = func; + } + + internal DataFrameColumn Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + long length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]], + (T6)columns[argOffsets[5]], + (T7)columns[argOffsets[6]], + (T8)columns[argOffsets[7]]); + } + + return CreateEmptyColumn(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class DataFrameUdfWrapper : DataFrameUdfWrapper + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where T9 : DataFrameColumn + where TResult : DataFrameColumn + { + private readonly Func _func; + + internal DataFrameUdfWrapper(Func func) + { + _func = func; + } + + internal DataFrameColumn Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + long length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]], + (T6)columns[argOffsets[5]], + (T7)columns[argOffsets[6]], + (T8)columns[argOffsets[7]], + (T9)columns[argOffsets[8]]); + } + + return CreateEmptyColumn(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the type of the tenth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal sealed class DataFrameUdfWrapper : DataFrameUdfWrapper + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where T9 : DataFrameColumn + where T10 : DataFrameColumn + where TResult : DataFrameColumn + { + private readonly Func _func; + + internal DataFrameUdfWrapper(Func func) + { + _func = func; + } + + internal DataFrameColumn Execute(ReadOnlyMemory input, int[] argOffsets) + { + ReadOnlySpan columns = input.Span; + long length = columns[0]?.Length ?? 0; + + if (length > 0) + { + return _func( + (T1)columns[argOffsets[0]], + (T2)columns[argOffsets[1]], + (T3)columns[argOffsets[2]], + (T4)columns[argOffsets[3]], + (T5)columns[argOffsets[4]], + (T6)columns[argOffsets[5]], + (T7)columns[argOffsets[6]], + (T8)columns[argOffsets[7]], + (T9)columns[argOffsets[8]], + (T10)columns[argOffsets[9]]); + } + + return CreateEmptyColumn(); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/DataFrameWorkerFunction.cs b/src/spark/Flowthru.Spark/Sql/DataFrameWorkerFunction.cs new file mode 100644 index 00000000..e604533b --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/DataFrameWorkerFunction.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Data.Analysis; +using FxDataFrame = Microsoft.Data.Analysis.DataFrame; + +namespace Flowthru.Spark.Sql +{ + /// + /// Function that will be executed in the worker using a that supports the Apache Arrow format. + /// + internal sealed class DataFrameWorkerFunction : WorkerFunction + { + /// + /// Type of the UDF to run. Refer to .Execute. + /// + /// unpickled data, representing a row + /// offsets to access input + /// + internal delegate DataFrameColumn ExecuteDelegate( + ReadOnlyMemory input, + int[] argOffsets); + + internal DataFrameWorkerFunction(ExecuteDelegate func) + { + Func = func; + } + + internal ExecuteDelegate Func { get; } + + /// + /// Used to chain functions. + /// + internal static DataFrameWorkerFunction Chain( + DataFrameWorkerFunction innerWorkerFunction, + DataFrameWorkerFunction outerWorkerFunction) + { + return new DataFrameWorkerFunction( + new WorkerFuncChainHelper( + innerWorkerFunction.Func, + outerWorkerFunction.Func).Execute); + } + + private class WorkerFuncChainHelper + { + private readonly ExecuteDelegate _innerFunc; + private readonly ExecuteDelegate _outerFunc; + + /// + /// The outer function will always take 0 as an offset since there is only one + /// return value from an inner function. + /// + private static readonly int[] s_outerFuncArgOffsets = { 0 }; + + internal WorkerFuncChainHelper(ExecuteDelegate inner, ExecuteDelegate outer) + { + _innerFunc = inner; + _outerFunc = outer; + } + + internal DataFrameColumn Execute( + ReadOnlyMemory input, + int[] argOffsets) + { + // For chaining, create an array with one element, which is a result from the inner + // function. Only the inner function will expect the given offsets, and the outer + // function will always take 0 as an offset since there is only one value in the + // input. + return _outerFunc( + new[] { _innerFunc(input, argOffsets) }, + s_outerFuncArgOffsets); + } + } + } + + /// + /// Function for Grouped Map Vector UDFs using a that supports the Apache Arrow format. + /// + internal sealed class DataFrameGroupedMapWorkerFunction : WorkerFunction + { + /// + /// A delegate to invoke a Grouped Map Vector UDF. + /// + /// The input data frame. + /// The resultant data frame. + internal delegate FxDataFrame ExecuteDelegate(FxDataFrame input); + + internal DataFrameGroupedMapWorkerFunction(ExecuteDelegate func) + { + Func = func; + } + + internal ExecuteDelegate Func { get; } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/DataFrameWriter.cs b/src/spark/Flowthru.Spark/Sql/DataFrameWriter.cs new file mode 100644 index 00000000..d36d9f86 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/DataFrameWriter.cs @@ -0,0 +1,238 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop.Internal.Java.Util; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql +{ + /// + /// Interface used to write a DataFrame to external storage systems (e.g. file systems, + /// key-value stores, etc). + /// + public sealed class DataFrameWriter : IJvmObjectReferenceProvider + { + internal DataFrameWriter(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + + /// + /// Specifies the behavior when data or table already exists. + /// + /// + /// Options include: + /// - SaveMode.Overwrite: overwrite the existing data. + /// - SaveMode.Append: append the data. + /// - SaveMode.Ignore: ignore the operation (i.e. no-op). + /// - SaveMode.ErrorIfExists: default option, throw an exception at runtime. + /// + /// Save mode enum + /// This DataFrameWriter object + public DataFrameWriter Mode(SaveMode saveMode) => Mode(saveMode.ToString()); + + /// + /// Specifies the behavior when data or table already exists. + /// + /// + /// Options include: + /// - "overwrite": overwrite the existing data. + /// - "append": append the data. + /// - "ignore": ignore the operation (i.e.no-op). + /// - "error" or "errorifexists": default option, throw an exception at runtime. + /// + /// Save mode string + /// This DataFrameWriter object + public DataFrameWriter Mode(string saveMode) + { + Reference.Invoke("mode", saveMode); + return this; + } + + /// + /// Specifies the underlying output data source. Built-in options include + /// "parquet", "json", etc. + /// + /// Data source name + /// This DataFrameWriter object + public DataFrameWriter Format(string source) + { + Reference.Invoke("format", source); + return this; + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataFrameWriter object + public DataFrameWriter Option(string key, string value) + { + return OptionInternal(key, value); + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataFrameWriter object + public DataFrameWriter Option(string key, bool value) + { + return OptionInternal(key, value); + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataFrameWriter object + public DataFrameWriter Option(string key, long value) + { + return OptionInternal(key, value); + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataFrameWriter object + public DataFrameWriter Option(string key, double value) + { + return OptionInternal(key, value); + } + + /// + /// Adds output options for the underlying data source. + /// + /// Key/value options + /// This DataFrameWriter object + public DataFrameWriter Options(Dictionary options) + { + Reference.Invoke("options", options); + return this; + } + + /// + /// Partitions the output by the given columns on the file system. If specified, + /// the output is laid out on the file system similar to Hive's partitioning scheme. + /// + /// Column names to partition by + /// This DataFrameWriter object + public DataFrameWriter PartitionBy(params string[] colNames) + { + Reference.Invoke("partitionBy", (object)colNames); + return this; + } + + /// + /// Buckets the output by the given columns. If specified, the output is laid out + /// on the file system similar to Hive's bucketing scheme. + /// + /// Number of buckets to save + /// A column name + /// Additional column names + /// This DataFrameWriter object + public DataFrameWriter BucketBy( + int numBuckets, + string colName, + params string[] colNames) + { + Reference.Invoke("bucketBy", numBuckets, colName, colNames); + return this; + } + + /// + /// Sorts the output in each bucket by the given columns. + /// + /// A name of a column + /// Additional column names + /// This DataFrameWriter object + public DataFrameWriter SortBy(string colName, params string[] colNames) + { + Reference.Invoke("sortBy", colName, colNames); + return this; + } + + /// + /// Saves the content of the DataFrame at the specified path. + /// + /// Path to save the content + public void Save(string path) => Reference.Invoke("save", path); + + /// + /// Saves the content of the DataFrame as the specified table. + /// + public void Save() => Reference.Invoke("save"); + + /// + /// Inserts the content of the DataFrame to the specified table. It requires that + /// the schema of the DataFrame is the same as the schema of the table. + /// + /// Name of the table + public void InsertInto(string tableName) => Reference.Invoke("insertInto", tableName); + + /// + /// Saves the content of the DataFrame as the specified table. + /// + /// Name of the table + public void SaveAsTable(string tableName) => Reference.Invoke("saveAsTable", tableName); + + /// + /// Saves the content of the DataFrame to a external database table via JDBC + /// + /// JDBC database URL of the form "jdbc:subprotocol:subname" + /// Name of the table in the external database + /// JDBC database connection arguments + public void Jdbc(string url, string table, Dictionary properties) + { + Reference.Invoke("jdbc", url, table, new Properties(Reference.Jvm, properties)); + } + + /// + /// Saves the content of the DataFrame in JSON format at the specified path. + /// + /// Path to save the content + public void Json(string path) => Reference.Invoke("json", path); + + /// + /// Saves the content of the DataFrame in Parquet format at the specified path. + /// + /// Path to save the content + public void Parquet(string path) => Reference.Invoke("parquet", path); + + /// + /// Saves the content of the DataFrame in ORC format at the specified path. + /// + /// Path to save the content + public void Orc(string path) => Reference.Invoke("orc", path); + + /// + /// Saves the content of the DataFrame in a text file at the specified path. + /// + /// Path to save the content + public void Text(string path) => Reference.Invoke("text", path); + + /// + /// Saves the content of the DataFrame in CSV format at the specified path. + /// + /// Path to save the content + public void Csv(string path) => Reference.Invoke("csv", path); + + /// + /// Helper function to add given key/value pair as a new option. + /// + /// Name of the option + /// Value of the option + /// This DataFrameWriter object + private DataFrameWriter OptionInternal(string key, object value) + { + Reference.Invoke("option", key, value); + return this; + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/DataFrameWriterV2.cs b/src/spark/Flowthru.Spark/Sql/DataFrameWriterV2.cs new file mode 100644 index 00000000..d38f3ca1 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/DataFrameWriterV2.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql +{ + /// + /// Interface used to write a to external storage using the v2 + /// API. + /// + [Since(Versions.V3_0_0)] + public sealed class DataFrameWriterV2 : IJvmObjectReferenceProvider + { + internal DataFrameWriterV2(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + + /// + /// Specifies a provider for the underlying output data source. Spark's default catalog + /// supports "parquet", "json", etc. + /// + /// Provider name + /// This DataFrameWriterV2 object + public DataFrameWriterV2 Using(string provider) + { + Reference.Invoke("using", provider); + return this; + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// string value of the option + /// This DataFrameWriterV2 object + public DataFrameWriterV2 Option(string key, string value) + { + Reference.Invoke("option", key, value); + return this; + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// bool value of the option + /// This DataFrameWriterV2 object + public DataFrameWriterV2 Option(string key, bool value) + { + Reference.Invoke("option", key, value); + return this; + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// Long value of the option + /// This DataFrameWriterV2 object + public DataFrameWriterV2 Option(string key, long value) + { + Reference.Invoke("option", key, value); + return this; + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// Double value of the option + /// This DataFrameWriterV2 object + public DataFrameWriterV2 Option(string key, double value) + { + Reference.Invoke("option", key, value); + return this; + } + + /// + /// Adds output options for the underlying data source. + /// + /// Key/value options + /// This DataFrameWriterV2 object + public DataFrameWriterV2 Options(Dictionary options) + { + Reference.Invoke("options", options); + return this; + } + + /// + /// Add a table property. + /// + /// Name of property + /// Value of the property + /// This DataFrameWriterV2 object + public DataFrameWriterV2 TableProperty(string property, string value) + { + Reference.Invoke("tableProperty", property, value); + return this; + } + + /// + /// Partition the output table created by , + /// , or using the given columns or + /// transforms. + /// + /// Column name to partition on + /// Columns to partition on + /// This DataFrameWriterV2 object + public DataFrameWriterV2 PartitionedBy(Column column, params Column[] columns) + { + Reference.Invoke("partitionedBy", column, columns); + return this; + } + + /// + /// Create a new table from the contents of the data frame. + /// + public void Create() => Reference.Invoke("create"); + + /// + /// Replace an existing table with the contents of the data frame. + /// + public void Replace() => Reference.Invoke("replace"); + + /// + /// Create a new table or replace an existing table with the contents of the data frame. + /// + public void CreateOrReplace() => Reference.Invoke("createOrReplace"); + + /// + /// Append the contents of the data frame to the output table. + /// + public void Append() => Reference.Invoke("append"); + + /// + /// Overwrite rows matching the given filter condition with the contents of the data frame + /// in the output table. + /// + /// Condition filter to overwrite based on + public void Overwrite(Column condition) => Reference.Invoke("overwrite", condition); + + /// + /// Overwrite all partition for which the data frame contains at least one row with the + /// contents of the data frame in the output table. + /// + public void OverwritePartitions() => Reference.Invoke("overwritePartitions"); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Expressions/UserDefinedFunction.cs b/src/spark/Flowthru.Spark/Sql/Expressions/UserDefinedFunction.cs new file mode 100644 index 00000000..c3d905f6 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Expressions/UserDefinedFunction.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql.Types; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Sql.Expressions +{ + /// + /// UserDefinedFunction is not exposed to the user directly. + /// Use Functions.Udf to create this object indirectly. + /// + internal sealed class UserDefinedFunction : IJvmObjectReferenceProvider + { + internal static UserDefinedFunction Create( + string name, + byte[] command, + UdfUtils.PythonEvalType evalType, + string returnType) + { + return Create(SparkEnvironment.JvmBridge, name, command, evalType, returnType); + } + + internal static UserDefinedFunction Create( + IJvmBridge jvm, + string name, + byte[] command, + UdfUtils.PythonEvalType evalType, + string returnType) + { + return new UserDefinedFunction( + jvm.CallConstructor( + "org.apache.spark.sql.execution.python.UserDefinedPythonFunction", + name, + UdfUtils.CreatePythonFunction(jvm, command), + DataType.FromJson(jvm, returnType), + (int)evalType, + true // udfDeterministic + )); + } + + internal UserDefinedFunction(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + // Apply0 and Apply10 is defined so that a corresponding Func<> can + // be constructed in Functions.Udf. + + internal Column Apply0() + { + return Apply(); + } + + internal Column Apply1(Column col) + { + return Apply(col); + } + + internal Column Apply2(Column col1, Column col2) + { + return Apply(col1, col2); + } + + internal Column Apply3(Column col1, Column col2, Column col3) + { + return Apply(col1, col2, col3); + } + + internal Column Apply4(Column col1, Column col2, Column col3, Column col4) + { + return Apply(col1, col2, col3, col4); + } + + internal Column Apply5(Column col1, Column col2, Column col3, Column col4, Column col5) + { + return Apply(col1, col2, col3, col4, col5); + } + + internal Column Apply6( + Column col1, + Column col2, + Column col3, + Column col4, + Column col5, + Column col6) + { + return Apply(col1, col2, col3, col4, col5, col6); + } + + internal Column Apply7( + Column col1, + Column col2, + Column col3, + Column col4, + Column col5, + Column col6, + Column col7) + { + return Apply(col1, col2, col3, col4, col5, col6, col7); + } + + internal Column Apply8( + Column col1, + Column col2, + Column col3, + Column col4, + Column col5, + Column col6, + Column col7, + Column col8) + { + return Apply(col1, col2, col3, col4, col5, col6, col7, col8); + } + + internal Column Apply9( + Column col1, + Column col2, + Column col3, + Column col4, + Column col5, + Column col6, + Column col7, + Column col8, + Column col9) + { + return Apply(col1, col2, col3, col4, col5, col6, col7, col8, col9); + } + + internal Column Apply10( + Column col1, + Column col2, + Column col3, + Column col4, + Column col5, + Column col6, + Column col7, + Column col8, + Column col9, + Column col10) + { + return Apply(col1, col2, col3, col4, col5, col6, col7, col8, col9, col10); + } + + internal Column Apply(params Column[] columns) + { + return new Column((JvmObjectReference)Reference.Invoke("apply", (object)columns)); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Expressions/Window.cs b/src/spark/Flowthru.Spark/Sql/Expressions/Window.cs new file mode 100644 index 00000000..9b59440a --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Expressions/Window.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql.Expressions +{ + /// + /// Utility functions for defining window in DataFrames. + /// + public static class Window + { + private static IJvmBridge Jvm { get; } = SparkEnvironment.JvmBridge; + private static readonly string s_windowClassName = + "org.apache.spark.sql.expressions.Window"; + + /// + /// Value representing the first row in the partition, equivalent to + /// "UNBOUNDED PRECEDING" in SQL. + /// + public static long UnboundedPreceding => + (long)Jvm.CallStaticJavaMethod(s_windowClassName, "unboundedPreceding"); + + /// + /// Value representing the last row in the partition, equivalent to + /// "UNBOUNDED FOLLOWING" in SQL. + /// + public static long UnboundedFollowing => + (long)Jvm.CallStaticJavaMethod(s_windowClassName, "unboundedFollowing"); + + /// + /// Value representing the current row. + /// + public static long CurrentRow => + (long)Jvm.CallStaticJavaMethod(s_windowClassName, "currentRow"); + + /// + /// Creates a `WindowSpec` with the partitioning defined. + /// + /// Name of column + /// Additional column names + /// WindowSpec object + public static WindowSpec PartitionBy(string colName, params string[] colNames) => + Apply("partitionBy", colName, colNames); + + /// + /// Creates a `WindowSpec` with the partitioning defined. + /// + /// Column expressions + /// WindowSpec object + public static WindowSpec PartitionBy(params Column[] columns) => + Apply("partitionBy", (object)columns); + + /// + /// Creates a `WindowSpec` with the ordering defined. + /// + /// Name of column + /// Additional column names + /// WindowSpec object + public static WindowSpec OrderBy(string colName, params string[] colNames) => + Apply("orderBy", colName, colNames); + + /// + /// Creates a `WindowSpec` with the ordering defined. + /// + /// Column expressions + /// WindowSpec object + public static WindowSpec OrderBy(params Column[] columns) => + Apply("orderBy", (object)columns); + + /// + /// Creates a `WindowSpec` with the frame boundaries defined, + /// from `start` (inclusive) to `end` (inclusive). + /// + /// + /// Boundary start, inclusive. The frame is unbounded if this is + /// the minimum long value `Window.s_unboundedPreceding`. + /// + /// + /// Boundary end, inclusive. The frame is unbounded if this is the + /// maximum long value `Window.s_unboundedFollowing`. + /// + /// WindowSpec object + public static WindowSpec RowsBetween(long start, long end) => + Apply("rowsBetween", start, end); + + /// + /// Creates a `WindowSpec` with the frame boundaries defined, + /// from `start` (inclusive) to `end` (inclusive). + /// + /// + /// Boundary start, inclusive. The frame is unbounded if this is + /// the minimum long value `Window.s_unboundedPreceding`. + /// + /// + /// Boundary end, inclusive. The frame is unbounded if this is + /// maximum long value `Window.s_unboundedFollowing`. + /// + /// WindowSpec object + public static WindowSpec RangeBetween(long start, long end) => + Apply("rangeBetween", start, end); + + /// + /// Creates a `WindowSpec` with the frame boundaries defined, + /// from `start` (inclusive) to `end` (inclusive). + /// + /// + /// This API is deprecated in Spark 2.4 and removed in Spark 3.0. + /// + /// + /// Boundary start, inclusive. The frame is unbounded if the expression is + /// `Microsoft.Spark.Sql.Functions.UnboundedPreceding()` + /// + /// + /// Boundary end, inclusive. The frame is unbounded if the expression is + /// `Microsoft.Spark.Sql.Functions.UnboundedFollowing()` + /// + /// WindowSpec object + [Deprecated(Versions.V2_4_0)] + [Removed(Versions.V3_0_0)] + public static WindowSpec RangeBetween(Column start, Column end) => + Apply("rangeBetween", start, end); + + private static WindowSpec Apply(string methodName, object arg) + { + return new WindowSpec( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_windowClassName, + methodName, + arg)); + } + + private static WindowSpec Apply(string methodName, object arg1, object arg2) + { + return new WindowSpec( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_windowClassName, + methodName, + arg1, + arg2)); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Expressions/WindowSpec.cs b/src/spark/Flowthru.Spark/Sql/Expressions/WindowSpec.cs new file mode 100644 index 00000000..893b1f34 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Expressions/WindowSpec.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql.Expressions +{ + /// + /// A window specification that defines the partitioning, ordering, and frame boundaries. + /// + public sealed class WindowSpec : IJvmObjectReferenceProvider + { + internal WindowSpec(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Defines the partitioning columns in a `WindowSpec`. + /// + /// Name of column + /// Additional column names + /// WindowSpec object + public WindowSpec PartitionBy(string colName, params string[] colNames) => + WrapAsWindowSpec(Reference.Invoke("partitionBy", colName, colNames)); + + /// + /// Defines the partitioning columns in a `WindowSpec`. + /// + /// Column expressions + /// WindowSpec object + public WindowSpec PartitionBy(params Column[] columns) => + WrapAsWindowSpec(Reference.Invoke("partitionBy", (object)columns)); + + /// + /// Defines the ordering columns in a `WindowSpec`. + /// + /// Name of column + /// Additional column names + /// WindowSpec object + public WindowSpec OrderBy(string colName, params string[] colNames) => + WrapAsWindowSpec(Reference.Invoke("orderBy", colName, colNames)); + + /// + /// Defines the ordering columns in a `WindowSpec`. + /// + /// Column expressions + /// WindowSpec object + public WindowSpec OrderBy(params Column[] columns) => + WrapAsWindowSpec(Reference.Invoke("orderBy", (object)columns)); + + /// + /// Defines the frame boundaries, from `start` (inclusive) to `end` (inclusive). + /// + /// + /// Boundary start, inclusive. The frame is unbounded if this is + /// the minimum long value `Window.s_unboundedPreceding`. + /// + /// + /// Boundary end, inclusive. The frame is unbounded if this is the + /// maximum long value `Window.s_unboundedFollowing`. + /// + /// WindowSpec object + public WindowSpec RowsBetween(long start, long end) => + WrapAsWindowSpec(Reference.Invoke("rowsBetween", start, end)); + + /// + /// Defines the frame boundaries, from `start` (inclusive) to `end` (inclusive). + /// + /// + /// Boundary start, inclusive. The frame is unbounded if this is + /// the minimum long value `Window.s_unboundedPreceding`. + /// + /// + /// Boundary end, inclusive. The frame is unbounded if this is + /// maximum long value `Window.s_unboundedFollowing`. + /// + /// WindowSpec object + public WindowSpec RangeBetween(long start, long end) => + WrapAsWindowSpec(Reference.Invoke("rangeBetween", start, end)); + + /// + /// Defines the frame boundaries, from `start` (inclusive) to `end` (inclusive). + /// + /// + /// This API is deprecated in Spark 2.4 and removed in Spark 3.0. + /// + /// + /// Boundary start, inclusive. The frame is unbounded if the expression is + /// `Microsoft.Spark.Sql.Functions.UnboundedPreceding()` + /// + /// + /// Boundary end, inclusive. The frame is unbounded if the expression is + /// `Microsoft.Spark.Sql.Functions.UnboundedFollowing()` + /// + /// WindowSpec object + [Deprecated(Versions.V2_4_0)] + [Removed(Versions.V3_0_0)] + public WindowSpec RangeBetween(Column start, Column end) => + WrapAsWindowSpec(Reference.Invoke("rangeBetween", start, end)); + + private WindowSpec WrapAsWindowSpec(object obj) => new WindowSpec((JvmObjectReference)obj); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/ForeachWriter.cs b/src/spark/Flowthru.Spark/Sql/ForeachWriter.cs new file mode 100644 index 00000000..6fd4100b --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/ForeachWriter.cs @@ -0,0 +1,193 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Flowthru.Spark.Sql +{ + /// + /// Interface for writing custom logic to process data generated by a query. This is + /// often used to write the output of a streaming query to arbitrary storage systems. + /// + /// + /// + /// Any implementation of this interface will be used by Spark in the following way: + /// + /// + /// + /// A single instance of this class is responsible of all the data generated by a single task + /// in a query. In other words, one instance is responsible for processing one partition of the + /// data generated in a distributed manner. + /// + /// + /// + /// + /// Any implementation of this class must be because each + /// task will get a fresh serialized-deserialized copy of the provided object. Hence, it is + /// strongly recommended that any initialization for writing data (e.g.opening a connection or + /// starting a transaction) is done after the method has been + /// called, which signifies that the task is ready to generate data. + /// + /// + /// + /// + /// The lifecycle of the methods are as follows: + /// + /// For each partition with partitionId: + /// ... For each batch/epoch of streaming data(if its streaming query) with epochId: + /// ....... Method Open(partitionId, epochId) is called. + /// ....... If Open returns true: + /// ........... For each row in the partition and batch/epoch, method Process(row) is called. + /// ....... Method Close(errorOrNull) is called with error(if any) seen while processing rows. + /// + /// + /// + /// + /// + /// + /// + /// Important points to note: + /// + /// + /// + /// The partitionId and epochId can be used to deduplicate generated data + /// when failures cause reprocessing of some input data. This depends on the execution + /// mode of the query. If the streaming query is being executed in the micro-batch + /// mode, then every partition represented by a unique tuple(partition_id, epoch_id) + /// is guaranteed to have the same data. Hence, (partition_id, epoch_id) can be used + /// to deduplicate and/or transactionally commit data and achieve exactly-once + /// guarantees. However, if the streaming query is being executed in the continuous + /// mode, then this guarantee does not hold and therefore should not be used for + /// deduplication. + /// + /// + /// + /// + /// + public interface IForeachWriter + { + /// + /// Called when starting to process one partition of new data in the executor. + /// + /// The partition id. + /// A unique id for data deduplication. + /// True if successful, false otherwise. + bool Open(long partitionId, long epochId); + + /// + /// Called to process each in the executor side. This method + /// will be called only if Open returns true. + /// + /// The row to process. + void Process(Row row); + + /// + /// Called when stopping to process one partition of new data in the executor side. This is + /// guaranteed to be called either returns true or + /// false. However, won't be called in the following + /// cases: + /// + /// + /// + /// CLR/JVM crashes without throwing a . + /// + /// + /// + /// + /// throws an . + /// + /// + /// + /// + /// + /// The thrown during processing or null if there was no error. + /// + void Close(Exception errorOrNull); + } + + /// + /// Wraps a and calls the appropriate methods as decribed in + /// the lifecycle documentation for the interface. + /// + internal class ForeachWriterWrapper + { + private readonly IForeachWriter _foreachWriter; + + internal ForeachWriterWrapper(IForeachWriter foreachWriter) => + _foreachWriter = foreachWriter; + + internal IEnumerable Process(int partitionId, IEnumerable rows) + { + if (!TaskContextHolder.Get().LocalProperties.TryGetValue( + "streaming.sql.batchId", + out string epochIdStr) || !long.TryParse(epochIdStr, out long epochId)) + { + throw new Exception( + $"Could not get or parse batch id from TaskContext - batchId: {epochIdStr}"); + } + + Exception error = null; + bool opened = _foreachWriter.Open(partitionId, epochId); + + try + { + if (opened) + { + foreach (Row row in rows) + { + _foreachWriter.Process(row); + } + } + } + catch (Exception e) + { + error = e; + } + finally + { + _foreachWriter.Close(error); + + if (error != null) + { + throw error; + } + } + + // An empty IEnumerable is returned because ForEach is a sink operation, + // but something needs to be returned to work within the UDF framework. + return Enumerable.Empty(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// When this UdfWrapper is processed, the PythonEvalType is + /// . The CommandExecutor expects the + /// method to match the + /// delegate. This UdfWrapper helps map + /// the UDF for to . + /// + [UdfWrapper] + internal class ForeachWriterWrapperUdfWrapper + { + private readonly Func, IEnumerable> _func; + + internal ForeachWriterWrapperUdfWrapper( + Func, IEnumerable> func) + { + _func = func; + } + + internal IEnumerable Execute(int pid, IEnumerable input) + { + // input is an IEnumerable, where each Row[] is batched using the + // org.apache.spark.api.python.SerDeUtil.AutoBatchedPickler algorithm. + // Refer to spark/sql/core/src/main/scala/org/apache/spark/sql/execution/python/ + // PythonForeachWriter.scala for more information. + return _func(pid, input.Cast().SelectMany(row => row)); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Functions.cs b/src/spark/Flowthru.Spark/Sql/Functions.cs new file mode 100644 index 00000000..5955eef3 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Functions.cs @@ -0,0 +1,4735 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Apache.Arrow; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql.Expressions; +using Flowthru.Spark.Sql.Types; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Sql +{ + /// + /// Functions available for DataFrame operations. + /// + public static class Functions + { + private static IJvmBridge Jvm { get; } = SparkEnvironment.JvmBridge; + private static readonly string s_functionsClassName = "org.apache.spark.sql.functions"; + + /// + /// Returns a Column based on the given column name. + /// + /// Column name + /// Column object + public static Column Column(string columnName) + { + return ApplyFunction("col", columnName); + } + + /// + /// Returns a Column based on the given column name. Alias for Column(). + /// + /// Column name + /// Column object + public static Column Col(string columnName) + { + return Column(columnName); + } + + /// + /// Creates a Column of literal value. + /// + /// Literal value + /// Column object + public static Column Lit(object literal) + { + return new Column( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_functionsClassName, + "lit", + literal)); + } + + ///////////////////////////////////////////////////////////////////////////////// + // Sort functions + ///////////////////////////////////////////////////////////////////////////////// + + /// + /// Returns a sort expression based on the ascending order of the column. + /// + /// Column name + /// Column object + public static Column Asc(string columnName) + { + return ApplyFunction("asc", columnName); + } + + /// + /// Returns a sort expression based on the ascending order of the column, + /// and null values return before non-null values. + /// + /// Column name + /// Column object + public static Column AscNullsFirst(string columnName) + { + return ApplyFunction("asc_nulls_first", columnName); + } + + /// + /// Returns a sort expression based on the ascending order of the column, + /// and null values appear after non-null values. + /// + /// Column name + /// Column object + public static Column AscNullsLast(string columnName) + { + return ApplyFunction("asc_nulls_last", columnName); + } + + /// + /// Returns a sort expression based on the descending order of the column. + /// + /// Column name + /// Column object + public static Column Desc(string columnName) + { + return ApplyFunction("desc", columnName); + } + + /// + /// Returns a sort expression based on the descending order of the column, + /// and null values return before non-null values. + /// + /// Column name + /// Column object + public static Column DescNullsFirst(string columnName) + { + return ApplyFunction("desc_nulls_first", columnName); + } + + /// + /// Returns a sort expression based on the descending order of the column, + /// and null values appear after non-null values. + /// + /// Column name + /// Column object + public static Column DescNullsLast(string columnName) + { + return ApplyFunction("desc_nulls_last", columnName); + } + + ///////////////////////////////////////////////////////////////////////////////// + // Aggregate functions + ///////////////////////////////////////////////////////////////////////////////// + + /// + /// Returns the approximate number of distinct items in a group. + /// + /// Column to apply + /// Column object + public static Column ApproxCountDistinct(Column column) + { + return ApplyFunction("approx_count_distinct", column); + } + + /// + /// Returns the approximate number of distinct items in a group. + /// + /// Column name + /// Column object + public static Column ApproxCountDistinct(string columnName) + { + return ApplyFunction("approx_count_distinct", columnName); + } + + /// + /// Returns the approximate number of distinct items in a group. + /// + /// Column to apply + /// Maximum estimation error allowed + /// Column object + public static Column ApproxCountDistinct(Column column, double rsd) + { + return ApplyFunction("approx_count_distinct", column, rsd); + } + + /// + /// Returns the approximate number of distinct items in a group. + /// + /// Column name + /// Maximum estimation error allowed + /// Column object + public static Column ApproxCountDistinct(string columnName, double rsd) + { + return ApplyFunction("approx_count_distinct", columnName, rsd); + } + + /// + /// Returns the average of the values in a group. + /// + /// Column to apply + /// Column object + public static Column Avg(Column column) + { + return ApplyFunction("avg", column); + } + + /// + /// Returns the average of the values in a group. + /// + /// Column name + /// Column object + public static Column Avg(string columnName) + { + return ApplyFunction("avg", columnName); + } + + /// + /// Returns a list of objects with duplicates. + /// + /// Column to apply + /// Column object + public static Column CollectList(Column column) + { + return ApplyFunction("collect_list", column); + } + + /// + /// Returns a list of objects with duplicates. + /// + /// Column name + /// Column object + public static Column CollectList(string columnName) + { + return ApplyFunction("collect_list", columnName); + } + + /// + /// Returns a set of objects with duplicate elements eliminated. + /// + /// Column to apply + /// Column object + public static Column CollectSet(Column column) + { + return ApplyFunction("collect_set", column); + } + + /// + /// Returns a set of objects with duplicate elements eliminated. + /// + /// Column name + /// Column object + public static Column CollectSet(string columnName) + { + return ApplyFunction("collect_set", columnName); + } + + /// + /// Returns the Pearson Correlation Coefficient for two columns. + /// + /// Column one to apply + /// Column two to apply + /// Column object + public static Column Corr(Column column1, Column column2) + { + return ApplyFunction("corr", column1, column2); + } + + /// + /// Returns the Pearson Correlation Coefficient for two columns. + /// + /// Column one name + /// Column two name + /// Column object + public static Column Corr(string columnName1, string columnName2) + { + return ApplyFunction("corr", columnName1, columnName2); + } + + /// + /// Returns the number of items in a group. + /// + /// Column to apply + /// Column object + public static Column Count(Column column) + { + return ApplyFunction("count", column); + } + + /// + /// Returns the number of items in a group. + /// + /// Column name + /// Column object + public static Column Count(string columnName) + { + return ApplyFunction("count", columnName); + } + + /// + /// Returns the number of distinct items in a group. + /// An alias of `Count_Distinct`, and it is encouraged to use `Count_Distinct` directly. + /// + /// Column to apply + /// Additional columns to apply + /// Column object + public static Column CountDistinct(Column column, params Column[] columns) + { + return ApplyFunction("countDistinct", column, columns); + } + + /// + /// Returns the number of distinct items in a group. + /// + /// Column name + /// Additional column names + /// Column object + public static Column CountDistinct(string columnName, params string[] columnNames) + { + return ApplyFunction("countDistinct", columnName, columnNames); + } + + /// + /// Returns the number of distinct items in a group. + /// + /// Column to apply + /// Additional columns to apply + /// Column object + [Since(Versions.V3_2_0)] + public static Column Count_Distinct(Column column, params Column[] columns) + { + return ApplyFunction("count_distinct", column, columns); + } + + /// + /// Returns the population covariance for two columns. + /// + /// Column one to apply + /// Column two to apply + /// Column object + public static Column CovarPop(Column column1, Column column2) + { + return ApplyFunction("covar_pop", column1, column2); + } + + /// + /// Returns the population covariance for two columns. + /// + /// Column one name + /// Column two name + /// Column object + public static Column CovarPop(string columnName1, string columnName2) + { + return ApplyFunction("covar_pop", columnName1, columnName2); + } + + /// + /// Returns the sample covariance for two columns. + /// + /// Column one to apply + /// Column two to apply + /// Column object + public static Column CovarSamp(Column column1, Column column2) + { + return ApplyFunction("covar_samp", column1, column2); + } + + /// + /// Returns the sample covariance for two columns. + /// + /// Column one name + /// Column two name + /// Column object + public static Column CovarSamp(string columnName1, string columnName2) + { + return ApplyFunction("covar_samp", columnName1, columnName2); + } + + /// + /// Returns the first value of a column in a group. + /// + /// + /// The function by default returns the first values it sees. It will return + /// the first non-null value it sees when ignoreNulls is set to true. + /// If all values are null, then null is returned. + /// + /// Column to apply + /// To ignore null or not + /// Column object + public static Column First(Column column, bool ignoreNulls = false) + { + return ApplyFunction("first", column, ignoreNulls); + } + + /// + /// Returns the first value of a column in a group. + /// + /// + /// The function by default returns the first values it sees. It will return + /// the first non-null value it sees when ignoreNulls is set to true. + /// If all values are null, then null is returned. + /// + /// Column name + /// To ignore null or not + /// Column object + public static Column First(string columnName, bool ignoreNulls = false) + { + return ApplyFunction("first", columnName, ignoreNulls); + } + + /// + /// Indicates whether a specified column in a GROUP BY list is aggregated + /// or not, returning 1 for aggregated or 0 for not aggregated in the result set. + /// + /// Column to apply + /// Column object + public static Column Grouping(Column column) + { + return ApplyFunction("grouping", column); + } + + /// + /// Indicates whether a specified column in a GROUP BY list is aggregated + /// or not, returning 1 for aggregated or 0 for not aggregated in the result set. + /// + /// Column name + /// Column object + public static Column Grouping(string columnName) + { + return ApplyFunction("grouping", columnName); + } + + /// + /// Returns the number of distinct items in a group. + /// + /// + /// The list of columns should match with grouping columns exactly, or empty + /// (meaning all the grouping columns). + /// + /// Columns to apply + /// Column object + public static Column GroupingId(params Column[] columns) + { + return ApplyFunction("grouping_id", (object)columns); + } + + /// + /// Returns the number of distinct items in a group. + /// + /// + /// The list of columns should match with grouping columns exactly. + /// + /// Column name + /// Additional column names + /// Column object + public static Column GroupingId(string columnName, params string[] columnNames) + { + return ApplyFunction("grouping_id", columnName, columnNames); + } + + /// + /// Returns the kurtosis of the values in a group. + /// + /// Column to apply + /// Column object + public static Column Kurtosis(Column column) + { + return ApplyFunction("kurtosis", column); + } + + /// + /// Returns the kurtosis of the values in a group. + /// + /// Column name + /// Column object + public static Column Kurtosis(string columnName) + { + return ApplyFunction("kurtosis", columnName); + } + + /// + /// Returns the last value of a column in a group. + /// + /// + /// The function by default returns the last values it sees. It will return + /// the last non-null value it sees when ignoreNulls is set to true. + /// If all values are null, then null is returned. + /// + /// Column to apply + /// To ignore null or not + /// Column object + public static Column Last(Column column, bool ignoreNulls = false) + { + return ApplyFunction("last", column, ignoreNulls); + } + + /// + /// Returns the last value of a column in a group. + /// + /// + /// The function by default returns the last values it sees. It will return + /// the last non-null value it sees when ignoreNulls is set to true. + /// If all values are null, then null is returned. + /// + /// Column name + /// To ignore null or not + /// Column object + public static Column Last(string columnName, bool ignoreNulls = false) + { + return ApplyFunction("last", columnName, ignoreNulls); + } + + /// + /// Returns the maximum value of the column in a group. + /// + /// Column to apply + /// Column object + public static Column Max(Column column) + { + return ApplyFunction("max", column); + } + + /// + /// Returns the maximum value of the column in a group. + /// + /// Column name + /// Column object + public static Column Max(string columnName) + { + return ApplyFunction("max", columnName); + } + + /// + /// Returns the average value of the column in a group. + /// + /// Column to apply + /// Column object + public static Column Mean(Column column) + { + return ApplyFunction("mean", column); + } + + /// + /// Returns the average value of the column in a group. + /// + /// Column name + /// Column object + public static Column Mean(string columnName) + { + return ApplyFunction("mean", columnName); + } + + /// + /// Returns the minimum value of the column in a group. + /// + /// Column to apply + /// Column object + public static Column Min(Column column) + { + return ApplyFunction("min", column); + } + + /// + /// Returns the minimum value of the column in a group. + /// + /// Column name + /// Column object + public static Column Min(string columnName) + { + return ApplyFunction("min", columnName); + } + + /// + /// Returns the approximate `percentile` of the numeric column `col` which + /// is the smallest value in the ordered `col` values (sorted from least to greatest) such that + /// no more than `percentage` of `col` values is less than the value or equal to that value. + /// + /// Column to apply + /// + /// If it is a single floating point value, it must be between 0.0 and 1.0. + /// When percentage is an array, each value of the percentage array must be between 0.0 and 1.0. + /// In this case, returns the approximate percentile array of column col + /// at the given percentage array. + /// + /// + /// Positive numeric literal which controls approximation accuracy at the cost of memory. + /// Higher value of accuracy yields better accuracy, 1.0/accuracy is the relative error of the + /// approximation. + /// + /// Column object + [Since(Versions.V3_1_0)] + public static Column PercentileApprox(Column column, Column percentage, Column accuracy) + { + return ApplyFunction("percentile_approx", column, percentage, accuracy); + } + + /// + /// Returns the product of all numerical elements in a group. + /// + /// Column to apply + /// Column object + [Since(Versions.V3_2_0)] + public static Column Product(Column column) + { + return ApplyFunction("product", column); + } + + /// + /// Returns the skewness of the values in a group. + /// + /// Column to apply + /// Column object + public static Column Skewness(Column column) + { + return ApplyFunction("skewness", column); + } + + /// + /// Returns the skewness of the values in a group. + /// + /// Column name + /// Column object + public static Column Skewness(string columnName) + { + return ApplyFunction("skewness", columnName); + } + + /// + /// Alias for StddevSamp(). + /// + /// Column to apply + /// Column object + public static Column Stddev(Column column) + { + return ApplyFunction("stddev", column); + } + + /// + /// Alias for StddevSamp(). + /// + /// Column name + /// Column object + public static Column Stddev(string columnName) + { + return ApplyFunction("stddev", columnName); + } + + /// + /// Returns the sample standard deviation of the expression in a group. + /// + /// Column to apply + /// Column object + public static Column StddevSamp(Column column) + { + return ApplyFunction("stddev_samp", column); + } + + /// + /// Returns the sample standard deviation of the expression in a group. + /// + /// Column name + /// Column object + public static Column StddevSamp(string columnName) + { + return ApplyFunction("stddev_samp", columnName); + } + + /// + /// Returns the population standard deviation of the expression in a group. + /// + /// Column to apply + /// Column object + public static Column StddevPop(Column column) + { + return ApplyFunction("stddev_pop", column); + } + + /// + /// Returns the population standard deviation of the expression in a group. + /// + /// Column name + /// Column object + public static Column StddevPop(string columnName) + { + return ApplyFunction("stddev_pop", columnName); + } + + /// + /// Returns the sum of all values in the expression. + /// + /// Column to apply + /// Column object + public static Column Sum(Column column) + { + return ApplyFunction("sum", column); + } + + /// + /// Returns the sum of all values in the expression. + /// + /// Column name + /// Column object + public static Column Sum(string columnName) + { + return ApplyFunction("sum", columnName); + } + + /// + /// Returns the sum of distinct values in the expression. + /// + /// Column to apply + /// Column object + [Deprecated(Versions.V3_2_0)] + public static Column SumDistinct(Column column) + { + return ApplyFunction("sumDistinct", column); + } + + /// + /// Returns the sum of distinct values in the expression. + /// + /// Column name + /// Column object + [Deprecated(Versions.V3_2_0)] + public static Column SumDistinct(string columnName) + { + return ApplyFunction("sumDistinct", columnName); + } + + /// + /// Returns the sum of distinct values in the expression. + /// + /// Column to apply + /// Column object + [Since(Versions.V3_2_0)] + public static Column Sum_Distinct(Column column) + { + return ApplyFunction("sum_distinct", column); + } + + /// + /// Alias for . + /// + /// Column to apply + /// Column object + public static Column Variance(Column column) + { + return ApplyFunction("variance", column); + } + + /// + /// Alias for . + /// + /// Column name + /// Column object + public static Column Variance(string columnName) + { + return ApplyFunction("variance", columnName); + } + + /// + /// Returns the unbiased variance of the values in a group. + /// + /// Column to apply + /// Column object + public static Column VarSamp(Column column) + { + return ApplyFunction("var_samp", column); + } + + /// + /// Returns the unbiased variance of the values in a group. + /// + /// Column name + /// Column object + public static Column VarSamp(string columnName) + { + return ApplyFunction("var_samp", columnName); + } + + /// + /// Returns the population variance of the values in a group. + /// + /// Column to apply + /// Column object + public static Column VarPop(Column column) + { + return ApplyFunction("var_pop", column); + } + + /// + /// Returns the population variance of the values in a group. + /// + /// Column name + /// Column object + public static Column VarPop(string columnName) + { + return ApplyFunction("var_pop", columnName); + } + + ///////////////////////////////////////////////////////////////////////////////// + // Window functions + ///////////////////////////////////////////////////////////////////////////////// + + /// + /// Window function: returns the special frame boundary that represents the first + /// row in the window partition. + /// + /// + /// This API is deprecated in Spark 2.4 and removed in Spark 3.0. + /// + /// Column object + [Deprecated(Versions.V2_4_0)] + [Removed(Versions.V3_0_0)] + public static Column UnboundedPreceding() + { + return ApplyFunction("unboundedPreceding"); + } + + /// + /// Window function: returns the special frame boundary that represents the last + /// row in the window partition. + /// + /// + /// This API is deprecated in Spark 2.4 and removed in Spark 3.0. + /// + /// Column object + [Deprecated(Versions.V2_4_0)] + [Removed(Versions.V3_0_0)] + public static Column UnboundedFollowing() + { + return ApplyFunction("unboundedFollowing"); + } + + /// + /// Window function: returns the special frame boundary that represents the current + /// row in the window partition. + /// + /// + /// This API is deprecated in Spark 2.4 and removed in Spark 3.0. + /// + /// Column object + [Deprecated(Versions.V2_4_0)] + [Removed(Versions.V3_0_0)] + public static Column CurrentRow() + { + return ApplyFunction("currentRow"); + } + + /// + /// Window function: returns the cumulative distribution of values within a window + /// partition, i.e. the fraction of rows that are below the current row. + /// + /// Column object + public static Column CumeDist() + { + return ApplyFunction("cume_dist"); + } + + /// + /// Window function: returns the rank of rows within a window partition, without any gaps. + /// + /// This is equivalent to the DENSE_RANK function in SQL. + /// Column object + public static Column DenseRank() + { + return ApplyFunction("dense_rank"); + } + + /// + /// Window function: returns the value that is 'offset' rows before the current row, + /// and null if there is less than 'offset' rows before the current row. + /// For example, an 'offset' of one will return the previous row at any given point + /// in the window partition. + /// + /// This is equivalent to the LAG function in SQL. + /// Column to apply + /// Offset from the current row + /// Default value when the offset row doesn't exist + /// Column object + public static Column Lag(Column column, int offset, object defaultValue = null) + { + return (defaultValue != null) ? + ApplyFunction("lag", column, offset, defaultValue) : + ApplyFunction("lag", column, offset); + } + + /// + /// Window function: returns the value that is 'offset' rows before the current row, + /// and null if there is less than 'offset' rows before the current row. + /// For example, an 'offset' of one will return the previous row at any given point + /// in the window partition. + /// + /// This is equivalent to the LAG function in SQL. + /// Column name + /// Offset from the current row + /// Default value when the offset row doesn't exist + /// Column object + public static Column Lag(string columnName, int offset, object defaultValue = null) + { + return (defaultValue != null) ? + ApplyFunction("lag", columnName, offset, defaultValue) : + ApplyFunction("lag", columnName, offset); + } + + /// + /// Window function: returns the value that is 'offset' rows before the current row, + /// and null if there is less than 'offset' rows before the current row. + /// 'ignoreNulls' determines whether null values of row are included in or eliminated from the + /// calculation. + /// For example, an 'offset' of one will return the previous row at any given point + /// in the window partition. + /// + /// Column to apply + /// Offset from the current row + /// Default value when the offset row doesn't exist + /// Boolean to determine whether null values are included or not + /// Column object + [Since(Versions.V3_2_0)] + public static Column Lag(Column column, int offset, object defaultValue, bool ignoreNulls) + { + return ApplyFunction("lag", column, offset, defaultValue, ignoreNulls); + } + + /// + /// Window function: returns the value that is 'offset' rows after the current row, + /// and null if there is less than 'offset' rows after the current row. + /// For example, an 'offset' of one will return the next row at any given point + /// in the window partition. + /// + /// This is equivalent to the LEAD function in SQL. + /// Column to apply + /// Offset from the current row + /// Default value when the offset row doesn't exist + /// Column object + public static Column Lead(Column column, int offset, object defaultValue = null) + { + return (defaultValue != null) ? + ApplyFunction("lead", column, offset, defaultValue) : + ApplyFunction("lead", column, offset); + } + + /// + /// Window function: returns the value that is 'offset' rows after the current row, + /// and null if there is less than 'offset' rows after the current row. + /// For example, an 'offset' of one will return the next row at any given point + /// in the window partition. + /// + /// This is equivalent to the LEAD function in SQL. + /// Column name + /// Offset from the current row + /// Default value when the offset row doesn't exist + /// Column object + public static Column Lead(string columnName, int offset, object defaultValue = null) + { + return (defaultValue != null) ? + ApplyFunction("lead", columnName, offset, defaultValue) : + ApplyFunction("lead", columnName, offset); + } + + /// + /// Window function: returns the value that is 'offset' rows after the current row, + /// and null if there is less than 'offset' rows after the current row. + /// 'ignoreNulls' determines whether null values of row are included in or eliminated from the + /// calculation. + /// For example, an 'offset' of one will return the next row at any given point + /// in the window partition. + /// + /// Column to apply + /// Offset from the current row + /// Default value when the offset row doesn't exist + /// Boolean to determine whether null values are included or not + /// Column object + [Since(Versions.V3_2_0)] + public static Column Lead(Column column, int offset, object defaultValue, bool ignoreNulls) + { + return ApplyFunction("lead", column, offset, defaultValue, ignoreNulls); + } + + /// + /// Returns the value that is the `offset`th row of the window frame + /// (counting from 1), and `null` if the size of window frame is less than `offset` rows. + /// + /// It will return the `offset`th non-null value it sees when ignoreNulls is set to true. + /// If all values are null, then null is returned. + /// + /// This is equivalent to the nth_value function in SQL. + /// + /// Column to apply + /// Offset from the current row + /// To ignore null or not + /// Column object + [Since(Versions.V3_1_0)] + public static Column NthValue(Column column, int offset, bool ignoreNulls = false) + { + return ApplyFunction("nth_value", column, offset, ignoreNulls); + } + + /// + /// Window function: returns the ntile group id (from 1 to `n` inclusive) in an ordered + /// window partition. For example, if `n` is 4, the first quarter of the rows will get + /// value 1, the second quarter will get 2, the third quarter will get 3, and the last + /// quarter will get 4. + /// + /// This is equivalent to the NTILE function in SQL. + /// Number of buckets + /// Column object + public static Column Ntile(int n) + { + return ApplyFunction("ntile", n); + } + + /// + /// Window function: returns the relative rank (i.e. percentile) of rows within + /// a window partition. + /// + /// This is equivalent to the PERCENT_RANK function in SQL. + /// Column object + public static Column PercentRank() + { + return ApplyFunction("percent_rank"); + } + + /// + /// Window function: returns the rank of rows within a window partition. + /// + /// This is equivalent to the RANK function in SQL. + /// Column object + public static Column Rank() + { + return ApplyFunction("rank"); + } + + /// + /// Window function: returns a sequential number starting at 1 within a window partition. + /// + /// Column object + public static Column RowNumber() + { + return ApplyFunction("row_number"); + } + + ///////////////////////////////////////////////////////////////////////////////// + // Non-aggregate functions + ///////////////////////////////////////////////////////////////////////////////// + + /// + /// Computes the absolute value. + /// + /// Column to apply + /// Column object + public static Column Abs(Column column) + { + return ApplyFunction("abs", column); + } + + /// + /// Creates a new array column. The input columns must all have the same data type. + /// + /// Columns to apply + /// Column object + public static Column Array(params Column[] columns) + { + return ApplyFunction("array", (object)columns); + } + + /// + /// Creates a new array column. The input columns must all have the same data type. + /// + /// Column name + /// Additional column names + /// Column object + public static Column Array(string columnName, params string[] columnNames) + { + return ApplyFunction("array", columnName, columnNames); + } + + /// + /// Creates a new map column. + /// + /// + /// The input columns must be grouped as key-value pairs, e.g. + /// (key1, value1, key2, value2, ...). The key columns must all have the same data type, + /// and can't be null. The value columns must all have the same data type. + /// + /// Columns to apply + /// Column object + public static Column Map(params Column[] columns) + { + return ApplyFunction("map", (object)columns); + } + + /// + /// Creates a new map column. The array in the first column is used for keys. The array + /// in the second column is used for values. All elements in the array for key should + /// not be null. + /// + /// Column expression for key + /// Column expression for values + /// Column object + [Since(Versions.V2_4_0)] + public static Column MapFromArrays(Column key, Column values) + { + return ApplyFunction("map_from_arrays", key, values); + } + + /// + /// Marks a DataFrame as small enough for use in broadcast joins. + /// + /// DataFrame to apply + /// DataFrame object + public static DataFrame Broadcast(DataFrame df) + { + return new DataFrame( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_functionsClassName, + "broadcast", + df)); + } + + /// + /// Returns the first column that is not null, or null if all inputs are null. + /// + /// Columns to apply + /// Column object + public static Column Coalesce(params Column[] columns) + { + return ApplyFunction("coalesce", (object)columns); + } + + /// + /// Creates a string column for the file name of the current Spark task. + /// + /// Column object + public static Column InputFileName() + { + return ApplyFunction("input_file_name"); + } + + /// + /// Return true iff the column is NaN. + /// + /// Column to apply + /// Column object + public static Column IsNaN(Column column) + { + return ApplyFunction("isnan", column); + } + + /// + /// Return true iff the column is null. + /// + /// Column to apply + /// Column object + public static Column IsNull(Column column) + { + return ApplyFunction("isnull", column); + } + + /// + /// A column expression that generates monotonically increasing 64-bit integers. + /// + /// Column object + public static Column MonotonicallyIncreasingId() + { + return ApplyFunction("monotonically_increasing_id"); + } + + /// + /// Returns col1 if it is not NaN, or col2 if col1 is NaN. + /// + /// + /// Both inputs should be floating point columns (DoubleType or FloatType). + /// + /// Column one to apply + /// Column two to apply + /// Column object + public static Column NaNvl(Column column1, Column column2) + { + return ApplyFunction("nanvl", column1, column2); + } + + /// + /// Unary minus, i.e. negate the expression. + /// + /// Column to apply + /// Column object + public static Column Negate(Column column) + { + return ApplyFunction("negate", column); + } + + /// + /// Inversion of boolean expression, i.e. NOT. + /// + /// Column to apply + /// Column object + public static Column Not(Column column) + { + return ApplyFunction("not", column); + } + + /// + /// Generate a random column with independent and identically distributed (i.i.d.) + /// samples from U[0.0, 1.0]. + /// + /// + /// This is non-deterministic when data partitions are not fixed. + /// + /// Random seed + /// Column object + public static Column Rand(long seed) + { + return ApplyFunction("rand", seed); + } + + /// + /// Generate a random column with independent and identically distributed (i.i.d.) + /// samples from U[0.0, 1.0]. + /// + /// Column object + public static Column Rand() + { + return ApplyFunction("rand"); + } + + /// + /// Generate a random column with independent and identically distributed (i.i.d.) + /// samples from the standard normal distribution. + /// + /// + /// This is non-deterministic when data partitions are not fixed. + /// + /// Random seed + /// Column object + public static Column Randn(long seed) + { + return ApplyFunction("randn", seed); + } + + /// + /// Generate a random column with independent and identically distributed (i.i.d.) + /// samples from the standard normal distribution. + /// + /// Column object + public static Column Randn() + { + return ApplyFunction("randn"); + } + + /// + /// Partition ID. + /// + /// + /// This is non-deterministic because it depends on data partitioning and task scheduling. + /// + /// Column object + public static Column SparkPartitionId() + { + return ApplyFunction("spark_partition_id"); + } + + /// + /// Computes the square root of the specified float value. + /// + /// Column to apply + /// Column object + public static Column Sqrt(Column column) + { + return ApplyFunction("sqrt", column); + } + + /// + /// Computes the square root of the specified float value. + /// + /// Column name + /// Column object + public static Column Sqrt(string columnName) + { + return ApplyFunction("sqrt", columnName); + } + + /// + /// Creates a new struct column that composes multiple input columns. + /// + /// Columns to apply + /// Column object + public static Column Struct(params Column[] columns) + { + return ApplyFunction("struct", (object)columns); + } + + /// + /// Creates a new struct column that composes multiple input columns. + /// + /// Column name + /// Additional column names + /// Column object + public static Column Struct(string columnName, params string[] columnNames) + { + return ApplyFunction("struct", columnName, columnNames); + } + + /// + /// Evaluates a condition and returns one of multiple possible result expressions. + /// If otherwise is not defined at the end, null is returned for + /// unmatched conditions. + /// + /// The condition to check. + /// The value to set if the condition is true. + /// Column object + public static Column When(Column condition, object value) + { + return ApplyFunction("when", condition, value); + } + + /// + /// Computes bitwise NOT. + /// + /// Column to apply + /// Column object + [Deprecated(Versions.V3_2_0)] + public static Column BitwiseNOT(Column column) + { + return ApplyFunction("bitwiseNOT", column); + } + + /// + /// Computes bitwise NOT of a number. + /// + /// Column to apply + /// Column object + [Since(Versions.V3_2_0)] + public static Column Bitwise_Not(Column column) + { + return ApplyFunction("bitwise_not", column); + } + + /// + /// Parses the expression string into the column that it represents. + /// + /// Expression string + /// Column object + public static Column Expr(string expr) + { + return ApplyFunction("expr", expr); + } + + ///////////////////////////////////////////////////////////////////////////////// + // Math functions + ///////////////////////////////////////////////////////////////////////////////// + + /// + /// Inverse cosine of `column` in radians, as if computed by `java.lang.Math.acos`. + /// + /// Column to apply + /// Column object + public static Column Acos(Column column) + { + return ApplyFunction("acos", column); + } + + /// + /// Inverse cosine of `columnName` in radians, as if computed by `java.lang.Math.acos`. + /// + /// Column name + /// Column object + public static Column Acos(string columnName) + { + return ApplyFunction("acos", columnName); + } + + /// + /// Inverse hyperbolic cosine of . + /// + /// Column to apply + /// Column object + [Since(Versions.V3_1_0)] + public static Column Acosh(Column column) + { + return ApplyFunction("acosh", column); + } + + /// + /// Inverse hyperbolic cosine of . + /// + /// Column name + /// Column object + [Since(Versions.V3_1_0)] + public static Column Acosh(string columnName) + { + return ApplyFunction("acosh", columnName); + } + + /// + /// Inverse sine of `column` in radians, as if computed by `java.lang.Math.asin`. + /// + /// Column to apply + /// Column object + public static Column Asin(Column column) + { + return ApplyFunction("asin", column); + } + + /// + /// Inverse sine of `columnName` in radians, as if computed by `java.lang.Math.asin`. + /// + /// Column name + /// Column object + public static Column Asin(string columnName) + { + return ApplyFunction("asin", columnName); + } + + /// + /// Inverse hyperbolic sine of . + /// + /// Column to apply + /// Column object + [Since(Versions.V3_1_0)] + public static Column Asinh(Column column) + { + return ApplyFunction("asinh", column); + } + + /// + /// Inverse hyperbolic sine of . + /// + /// Column name + /// Column object + [Since(Versions.V3_1_0)] + public static Column Asinh(string columnName) + { + return ApplyFunction("asinh", columnName); + } + + /// + /// Inverse tangent of `column` in radians, as if computed by `java.lang.Math.atan`. + /// + /// Column to apply + /// Column object + public static Column Atan(Column column) + { + return ApplyFunction("atan", column); + } + + /// + /// Inverse tangent of `columnName` in radians, as if computed by `java.lang.Math.atan`. + /// + /// Column name + /// Column object + public static Column Atan(string columnName) + { + return ApplyFunction("atan", columnName); + } + + /// + /// Computes atan2 for the given `x` and `y`. + /// + /// Coordinate on y-axis + /// Coordinate on x-axis + /// Column object + public static Column Atan2(Column y, Column x) + { + return ApplyFunction("atan2", y, x); + } + + /// + /// Computes atan2 for the given `x` and `y`. + /// + /// Coordinate on y-axis + /// Coordinate on x-axis + /// Column object + public static Column Atan2(Column y, string xName) + { + return ApplyFunction("atan2", y, xName); + } + + /// + /// Computes atan2 for the given `x` and `y`. + /// + /// Coordinate on y-axis + /// Coordinate on x-axis + /// Column object + public static Column Atan2(string yName, Column x) + { + return ApplyFunction("atan2", yName, x); + } + + /// + /// Computes atan2 for the given `x` and `y`. + /// + /// Coordinate on y-axis + /// Coordinate on x-axis + /// Column object + public static Column Atan2(string yName, string xName) + { + return ApplyFunction("atan2", yName, xName); + } + + /// + /// Computes atan2 for the given `x` and `y`. + /// + /// Coordinate on y-axis + /// Coordinate on x-axis + /// Column object + public static Column Atan2(Column y, double xValue) + { + return ApplyFunction("atan2", y, xValue); + } + + /// + /// Computes atan2 for the given `x` and `y`. + /// + /// Coordinate on y-axis + /// Coordinate on x-axis + /// Column object + public static Column Atan2(string yName, double xValue) + { + return ApplyFunction("atan2", yName, xValue); + } + + /// + /// Computes atan2 for the given `x` and `y`. + /// + /// Coordinate on y-axis + /// Coordinate on x-axis + /// Column object + public static Column Atan2(double yValue, Column x) + { + return ApplyFunction("atan2", yValue, x); + } + + /// + /// Computes atan2 for the given `x` and `y`. + /// + /// Coordinate on y-axis + /// Coordinate on x-axis + /// Column object + public static Column Atan2(double yValue, string xName) + { + return ApplyFunction("atan2", yValue, xName); + } + + /// + /// Inverse hyperbolic tangent of . + /// + /// Column to apply + /// Column object + [Since(Versions.V3_1_0)] + public static Column Atanh(Column column) + { + return ApplyFunction("atanh", column); + } + + /// + /// Inverse hyperbolic tangent of . + /// + /// Column name + /// Column object + [Since(Versions.V3_1_0)] + public static Column Atanh(string columnName) + { + return ApplyFunction("atanh", columnName); + } + + /// + /// An expression that returns the string representation of the binary value + /// of the given long column. For example, bin("12") returns "1100". + /// + /// Column to apply + /// Column object + public static Column Bin(Column column) + { + return ApplyFunction("bin", column); + } + + /// + /// An expression that returns the string representation of the binary value + /// of the given long column. For example, bin("12") returns "1100". + /// + /// Column name + /// Column object + public static Column Bin(string columnName) + { + return ApplyFunction("bin", columnName); + } + + /// + /// Computes the cube-root of the given column. + /// + /// Column to apply + /// Column object + public static Column Cbrt(Column column) + { + return ApplyFunction("cbrt", column); + } + + /// + /// Computes the cube-root of the given column. + /// + /// Column name + /// Column object + public static Column Cbrt(string columnName) + { + return ApplyFunction("cbrt", columnName); + } + + /// + /// Computes the ceiling of the given value. + /// + /// Column to apply + /// Column object + public static Column Ceil(Column column) + { + return ApplyFunction("ceil", column); + } + + /// + /// Computes the ceiling of the given value. + /// + /// Column name + /// Column object + public static Column Ceil(string columnName) + { + return ApplyFunction("ceil", columnName); + } + + /// + /// Convert a number in a string column from one base to another. + /// + /// Column to apply + /// Source base number + /// Target base number + /// Column object + public static Column Conv(Column column, int fromBase, int toBase) + { + return ApplyFunction("conv", column, fromBase, toBase); + } + + /// + /// Computes cosine of the angle, as if computed by `java.lang.Math.cos` + /// + /// Column to apply + /// Column object + public static Column Cos(Column column) + { + return ApplyFunction("cos", column); + } + + /// + /// Computes cosine of the angle, as if computed by `java.lang.Math.cos` + /// + /// Column name + /// Column object + public static Column Cos(string columnName) + { + return ApplyFunction("cos", columnName); + } + + /// + /// Computes hyperbolic cosine of the angle, as if computed by `java.lang.Math.cosh` + /// + /// Column to apply + /// Column object + public static Column Cosh(Column column) + { + return ApplyFunction("cosh", column); + } + + /// + /// Computes hyperbolic cosine of the angle, as if computed by `java.lang.Math.cosh` + /// + /// Column name + /// Column object + public static Column Cosh(string columnName) + { + return ApplyFunction("cosh", columnName); + } + + /// + /// Computes the exponential of the given value. + /// + /// Column to apply + /// Column object + public static Column Exp(Column column) + { + return ApplyFunction("exp", column); + } + + /// + /// Computes the exponential of the given value. + /// + /// Column name + /// Column object + public static Column Exp(string columnName) + { + return ApplyFunction("exp", columnName); + } + + /// + /// Computes the exponential of the given value minus one. + /// + /// Column to apply + /// Column object + public static Column Expm1(Column column) + { + return ApplyFunction("expm1", column); + } + + /// + /// Computes the exponential of the given value minus one. + /// + /// Column name + /// Column object + public static Column Expm1(string columnName) + { + return ApplyFunction("expm1", columnName); + } + + /// + /// Computes the factorial of the given value. + /// + /// Column to apply + /// Column object + public static Column Factorial(Column column) + { + return ApplyFunction("factorial", column); + } + + /// + /// Computes the floor of the given value. + /// + /// Column to apply + /// Column object + public static Column Floor(Column column) + { + return ApplyFunction("floor", column); + } + + /// + /// Computes the floor of the given value. + /// + /// Column name + /// Column object + public static Column Floor(string columnName) + { + return ApplyFunction("floor", columnName); + } + + /// + /// Returns the greatest value of the list of values, skipping null values. + /// + /// Columns to apply + /// Column object + public static Column Greatest(params Column[] columns) + { + return ApplyFunction("greatest", (object)columns); + } + + /// + /// Returns the greatest value of the list of column names, skipping null values. + /// + /// Column name + /// Additional column names + /// Column object + public static Column Greatest(string columnName, params string[] columnNames) + { + return ApplyFunction("greatest", columnName, columnNames); + } + + /// + /// Computes hex value of the given column. + /// + /// Column to apply + /// Column object + public static Column Hex(Column column) + { + return ApplyFunction("hex", column); + } + + /// + /// Inverse of hex. Interprets each pair of characters as a hexadecimal number + /// and converts to the byte representation of number. + /// + /// Column to apply + /// Column object + public static Column Unhex(Column column) + { + return ApplyFunction("unhex", column); + } + + /// + /// Computes `sqrt(a^2^ + b^2^)` without intermediate overflow or underflow. + /// + /// Left side column to apply + /// Right side column to apply + /// Column object + public static Column Hypot(Column left, Column right) + { + return ApplyFunction("hypot", left, right); + } + + /// + /// Computes `sqrt(a^2^ + b^2^)` without intermediate overflow or underflow. + /// + /// Left side column to apply + /// Right side column name + /// Column object + public static Column Hypot(Column left, string rightName) + { + return ApplyFunction("hypot", left, rightName); + } + + /// + /// Computes `sqrt(a^2^ + b^2^)` without intermediate overflow or underflow. + /// + /// Left side column name + /// Right side column to apply + /// Column object + public static Column Hypot(string leftName, Column right) + { + return ApplyFunction("hypot", leftName, right); + } + + /// + /// Computes `sqrt(a^2^ + b^2^)` without intermediate overflow or underflow. + /// + /// Left side column name + /// Right side column name + /// Column object + public static Column Hypot(string leftName, string rightName) + { + return ApplyFunction("hypot", leftName, rightName); + } + + /// + /// Computes `sqrt(a^2^ + b^2^)` without intermediate overflow or underflow. + /// + /// Left side column to apply + /// Right side value + /// Column object + public static Column Hypot(Column left, double right) + { + return ApplyFunction("hypot", left, right); + } + + /// + /// Computes `sqrt(a^2^ + b^2^)` without intermediate overflow or underflow. + /// + /// Left side column name + /// Right side value + /// Column object + public static Column Hypot(string leftName, double right) + { + return ApplyFunction("hypot", leftName, right); + } + + /// + /// Computes `sqrt(a^2^ + b^2^)` without intermediate overflow or underflow. + /// + /// Left side value + /// Right side column to apply + /// Column object + public static Column Hypot(double left, Column right) + { + return ApplyFunction("hypot", left, right); + } + + /// + /// Computes `sqrt(a^2^ + b^2^)` without intermediate overflow or underflow. + /// + /// Left side value + /// Right side column name + /// Column object + public static Column Hypot(double left, string rightName) + { + return ApplyFunction("hypot", left, rightName); + } + + /// + /// Returns the least value of the list of values, skipping null values. + /// + /// Columns to apply + /// Column object + public static Column Least(params Column[] columns) + { + return ApplyFunction("least", (object)columns); + } + + /// + /// Returns the least value of the list of values, skipping null values. + /// + /// Column name + /// Additional column names + /// Column object + public static Column Least(string columnName, params string[] columnNames) + { + return ApplyFunction("least", columnName, columnNames); + } + + /// + /// Computes the natural logarithm of the given value. + /// + /// Column to apply + /// Column object + public static Column Log(Column column) + { + return ApplyFunction("log", column); + } + + /// + /// Computes the natural logarithm of the given value. + /// + /// Column name + /// Column object + public static Column Log(string columnName) + { + return ApplyFunction("log", columnName); + } + + /// + /// Computes the first argument-base logarithm of the second argument. + /// + /// Base for logarithm + /// Column to apply + /// Column object + public static Column Log(double logBase, Column column) + { + return ApplyFunction("log", logBase, column); + } + + /// + /// Computes the first argument-base logarithm of the second argument. + /// + /// Base for logarithm + /// Column name + /// Column object + public static Column Log(double logBase, string columnName) + { + return ApplyFunction("log", logBase, columnName); + } + + /// + /// Computes the logarithm of the given value in base 10. + /// + /// Column to apply + /// Column object + public static Column Log10(Column column) + { + return ApplyFunction("log10", column); + } + + /// + /// Computes the logarithm of the given value in base 10. + /// + /// Column name + /// Column object + public static Column Log10(string columnName) + { + return ApplyFunction("log10", columnName); + } + + /// + /// Computes the natural logarithm of the given value plus one. + /// + /// Column to apply + /// Column object + public static Column Log1p(Column column) + { + return ApplyFunction("log1p", column); + } + + /// + /// Computes the natural logarithm of the given value plus one. + /// + /// Column name + /// Column object + public static Column Log1p(string columnName) + { + return ApplyFunction("log1p", columnName); + } + + /// + /// Computes the logarithm of the given column in base 2. + /// + /// Column to apply + /// Column object + public static Column Log2(Column column) + { + return ApplyFunction("log2", column); + } + + /// + /// Computes the logarithm of the given column in base 2. + /// + /// Column name + /// Column object + public static Column Log2(string columnName) + { + return ApplyFunction("log2", columnName); + } + + /// + /// Returns the value of the first argument raised to the power of the second argument. + /// + /// Left side column to apply + /// Right side column to apply + /// Column object + public static Column Pow(Column left, Column right) + { + return ApplyFunction("pow", left, right); + } + + /// + /// Returns the value of the first argument raised to the power of the second argument. + /// + /// Left side column to apply + /// Right side column name + /// Column object + public static Column Pow(Column left, string rightName) + { + return ApplyFunction("pow", left, rightName); + } + + /// + /// Returns the value of the first argument raised to the power of the second argument. + /// + /// Left side column name + /// Right side column to apply + /// Column object + public static Column Pow(string leftName, Column right) + { + return ApplyFunction("pow", leftName, right); + } + + /// + /// Returns the value of the first argument raised to the power of the second argument. + /// + /// Left side column name + /// Right side column name + /// Column object + public static Column Pow(string leftName, string rightName) + { + return ApplyFunction("pow", leftName, rightName); + } + + /// + /// Returns the value of the first argument raised to the power of the second argument. + /// + /// Left side column to apply + /// Right side value + /// Column object + public static Column Pow(Column left, double right) + { + return ApplyFunction("pow", left, right); + } + + /// + /// Returns the value of the first argument raised to the power of the second argument. + /// + /// Left side column name + /// Right side value + /// Column object + public static Column Pow(string leftName, double right) + { + return ApplyFunction("pow", leftName, right); + } + + /// + /// Returns the value of the first argument raised to the power of the second argument. + /// + /// Left side value + /// Right side column to apply + /// Column object + public static Column Pow(double left, Column right) + { + return ApplyFunction("pow", left, right); + } + + /// + /// Returns the value of the first argument raised to the power of the second argument. + /// + /// Left side value + /// Right side column name + /// Column object + public static Column Pow(double left, string rightName) + { + return ApplyFunction("pow", left, rightName); + } + + /// + /// Returns the positive value of dividend mod divisor. + /// + /// Left side column to apply + /// Right side column to apply + /// Column object + public static Column Pmod(Column left, Column right) + { + return ApplyFunction("pmod", left, right); + } + + /// + /// Returns the double value that is closest in value to the argument and + /// is equal to a mathematical integer. + /// + /// Column to apply + /// Column object + public static Column Rint(Column column) + { + return ApplyFunction("rint", column); + } + + /// + /// Returns the double value that is closest in value to the argument and + /// is equal to a mathematical integer. + /// + /// Column name + /// Column object + public static Column Rint(string columnName) + { + return ApplyFunction("rint", columnName); + } + + /// + /// Returns the value of the `column` rounded to 0 decimal places with + /// HALF_UP round mode. + /// + /// Column to apply + /// Column object + public static Column Round(Column column) + { + return ApplyFunction("round", column); + } + + /// + /// Returns the value of the `column` rounded to `scale` decimal places with + /// HALF_UP round mode. + /// + /// Column to apply + /// Scale factor + /// Column object + public static Column Round(Column column, int scale) + { + return ApplyFunction("round", column, scale); + } + + /// + /// Returns the value of the `column` rounded to 0 decimal places with + /// HALF_EVEN round mode. + /// + /// Column to apply + /// Column object + public static Column Bround(Column column) + { + return ApplyFunction("bround", column); + } + + /// + /// Returns the value of the `column` rounded to `scale` decimal places with + /// HALF_EVEN round mode. + /// + /// Column to apply + /// Scale factor + /// Column object + public static Column Bround(Column column, int scale) + { + return ApplyFunction("bround", column, scale); + } + + + /// + /// Shift the given value `numBits` left. + /// + /// Column to apply + /// Number of bits to shift + /// Column object + [Deprecated(Versions.V3_2_0)] + public static Column ShiftLeft(Column column, int numBits) + { + return ApplyFunction("shiftLeft", column, numBits); + } + + /// + /// Shift the given value `numBits` left. + /// + /// Column to apply + /// Number of bits to shift + /// Column object + [Since(Versions.V3_2_0)] + public static Column Shiftleft(Column column, int numBits) + { + return ApplyFunction("shiftleft", column, numBits); + } + + /// + /// (Signed) shift the given value `numBits` right. + /// + /// Column to apply + /// Number of bits to shift + /// Column object + [Deprecated(Versions.V3_2_0)] + public static Column ShiftRight(Column column, int numBits) + { + return ApplyFunction("shiftRight", column, numBits); + } + + /// + /// (Signed) shift the given value `numBits` right. + /// + /// Column to apply + /// Number of bits to shift + /// Column object + [Since(Versions.V3_2_0)] + public static Column Shiftright(Column column, int numBits) + { + return ApplyFunction("shiftright", column, numBits); + } + + /// + /// Unsigned shift the given value `numBits` right. + /// + /// Column to apply + /// Number of bits to shift + /// Column object + [Deprecated(Versions.V3_2_0)] + public static Column ShiftRightUnsigned(Column column, int numBits) + { + return ApplyFunction("shiftRightUnsigned", column, numBits); + } + + /// + /// Unsigned shift the given value `numBits` right. + /// + /// Column to apply + /// Number of bits to shift + /// Column object + [Since(Versions.V3_2_0)] + public static Column Shiftrightunsigned(Column column, int numBits) + { + return ApplyFunction("shiftrightunsigned", column, numBits); + } + + /// + /// Computes the signum of the given value. + /// + /// Column to apply + /// Column object + public static Column Signum(Column column) + { + return ApplyFunction("signum", column); + } + + /// + /// Computes the signum of the given value. + /// + /// Column name + /// Column object + public static Column Signum(string columnName) + { + return ApplyFunction("signum", columnName); + } + + /// + /// Computes sine of the angle, as if computed by `java.lang.Math.sin`. + /// + /// Column to apply + /// Column object + public static Column Sin(Column column) + { + return ApplyFunction("sin", column); + } + + /// + /// Computes sine of the angle, as if computed by `java.lang.Math.sin`. + /// + /// Column name + /// Column object + public static Column Sin(string columnName) + { + return ApplyFunction("sin", columnName); + } + + /// + /// Computes hyperbolic sine of the angle, as if computed by `java.lang.Math.sin`. + /// + /// Column to apply + /// Column object + public static Column Sinh(Column column) + { + return ApplyFunction("sinh", column); + } + + /// + /// Computes hyperbolic sine of the angle, as if computed by `java.lang.Math.sin`. + /// + /// Column name + /// Column object + public static Column Sinh(string columnName) + { + return ApplyFunction("sinh", columnName); + } + + /// + /// Computes tangent of the given value, as if computed by `java.lang.Math.tan`. + /// + /// Column to apply + /// Column object + public static Column Tan(Column column) + { + return ApplyFunction("tan", column); + } + + /// + /// Computes tangent of the given value, as if computed by `java.lang.Math.tan`. + /// + /// Column name + /// Column object + public static Column Tan(string columnName) + { + return ApplyFunction("tan", columnName); + } + + /// + /// Computes hyperbolic tangent of the given value, as if computed by + /// `java.lang.Math.tanh`. + /// + /// Column to apply + /// Column object + public static Column Tanh(Column column) + { + return ApplyFunction("tanh", column); + } + + /// + /// Computes hyperbolic tangent of the given value, as if computed by + /// `java.lang.Math.tanh`. + /// + /// Column name + /// Column object + public static Column Tanh(string columnName) + { + return ApplyFunction("tanh", columnName); + } + + /// + /// Converts an angle measured in radians to an approximately equivalent angle + /// measured in degrees. + /// + /// Column to apply + /// Column object + public static Column Degrees(Column column) + { + return ApplyFunction("degrees", column); + } + + /// + /// Converts an angle measured in radians to an approximately equivalent angle + /// measured in degrees. + /// + /// Column name + /// Column object + public static Column Degrees(string columnName) + { + return ApplyFunction("degrees", columnName); + } + + /// + /// Converts an angle measured in degrees to an approximately equivalent angle + /// measured in radians. + /// + /// Column to apply + /// Column object + public static Column Radians(Column column) + { + return ApplyFunction("radians", column); + } + + /// + /// Converts an angle measured in degrees to an approximately equivalent angle + /// measured in radians. + /// + /// Column name + /// Column object + public static Column Radians(string columnName) + { + return ApplyFunction("radians", columnName); + } + + ///////////////////////////////////////////////////////////////////////////////// + // Misc functions + ///////////////////////////////////////////////////////////////////////////////// + + /// + /// Calculates the MD5 digest of a binary column and returns the value + /// as a 32 character hex string. + /// + /// Column to apply + /// Column object + public static Column Md5(Column column) + { + return ApplyFunction("md5", column); + } + + /// + /// Calculates the SHA-1 digest of a binary column and returns the value + /// as a 40 character hex string. + /// + /// Column to apply + /// Column object + public static Column Sha1(Column column) + { + return ApplyFunction("sha1", column); + } + + /// + /// Calculates the SHA-2 family of hash functions of a binary column and + /// returns the value as a hex string. + /// + /// Column to apply + /// One of 224, 256, 384 or 512 + /// Column object + public static Column Sha2(Column column, int numBits) + { + return ApplyFunction("sha2", column, numBits); + } + + /// + /// Calculates the cyclic redundancy check value (CRC32) of a binary column and + /// returns the value as a bigint. + /// + /// Column to apply + /// Column object + public static Column Crc32(Column column) + { + return ApplyFunction("crc32", column); + } + + /// + /// Calculates the hash code of given columns, and returns the result as an int column. + /// + /// Columns to apply + /// Column object + public static Column Hash(params Column[] columns) + { + return ApplyFunction("hash", (object)columns); + } + + /// + /// Calculates the hash code of given columns using the 64-bit variant of the xxHash + /// algorithm, and returns the result as a long column. + /// + /// Columns to apply + /// Column object + [Since(Versions.V3_0_0)] + public static Column XXHash64(params Column[] columns) + { + return ApplyFunction("xxhash64", (object)columns); + } + + /// + /// Returns null if the condition is true, and throws an exception otherwise. + /// + /// Column to apply + /// Column object + [Since(Versions.V3_1_0)] + public static Column AssertTrue(Column column) + { + return ApplyFunction("assert_true", column); + } + + /// + /// Returns null if the condition is true; throws an exception with the error message otherwise. + /// + /// Column to apply + /// Error message + /// Column object + [Since(Versions.V3_1_0)] + public static Column AssertTrue(Column column, Column errMsg) + { + return ApplyFunction("assert_true", column, errMsg); + } + + /// + /// Throws an exception with the provided error message. + /// + /// Error message + /// Column object + [Since(Versions.V3_1_0)] + public static Column RaiseError(Column errMsg) + { + return ApplyFunction("raise_error", errMsg); + } + + ///////////////////////////////////////////////////////////////////////////////// + // String functions + ///////////////////////////////////////////////////////////////////////////////// + + /// + /// Computes the numeric value of the first character of the string column, and returns + /// the result as an int column. + /// + /// Column to apply + /// Column object + public static Column Ascii(Column column) + { + return ApplyFunction("ascii", column); + } + + /// + /// Computes the BASE64 encoding of a binary column and returns it as a string column. + /// + /// + /// This is the reverse of unbase64. + /// + /// Column to apply + /// Column object + public static Column Base64(Column column) + { + return ApplyFunction("base64", column); + } + + /// + /// Concatenates multiple input string columns together into a single string column, + /// using the given separator. + /// + /// Separator used for string concatenation + /// Columns to apply + /// Column object + public static Column ConcatWs(string sep, params Column[] columns) + { + return ApplyFunction("concat_ws", sep, columns); + } + + /// + /// Computes the first argument into a string from a binary using the provided + /// character set (one of 'US-ASCII', 'ISO-8859-1', 'UTF-8', 'UTF-16BE', 'UTF-16LE', + /// 'UTF-16') + /// + /// Column to apply + /// Character set + /// Column object + public static Column Decode(Column column, string charset) + { + return ApplyFunction("decode", column, charset); + } + + /// + /// Computes the first argument into a binary from a string using the provided + /// character set (one of 'US-ASCII', 'ISO-8859-1', 'UTF-8', 'UTF-16BE', 'UTF-16LE', + /// 'UTF-16') + /// + /// Column to apply + /// Character set + /// Column object + public static Column Encode(Column column, string charset) + { + return ApplyFunction("encode", column, charset); + } + + /// + /// Formats the given numeric `column` to a format like '#,###,###.##', + /// rounded to the given `d` decimal places with HALF_EVEN round mode, + /// and returns the result as a string column. + /// + /// Column to apply + /// Decimal places for rounding + /// Column object + public static Column FormatNumber(Column column, int d) + { + return ApplyFunction("format_number", column, d); + } + + /// + /// Formats the arguments in printf-style and returns the result as a string column. + /// + /// Printf-style format + /// Columns to apply + /// Column object + public static Column FormatString(string format, params Column[] columns) + { + return ApplyFunction("format_string", format, columns); + } + + /// + /// Returns a new string column by converting the first letter of each word to uppercase. + /// Words are delimited by whitespace. + /// + /// + /// + /// Column to apply + /// Column object + public static Column InitCap(Column column) + { + return ApplyFunction("initcap", column); + } + + /// + /// Locate the position of the first occurrence of the given substring. + /// + /// + /// The position is not zero based, but 1 based index. Returns 0 if the given substring + /// could not be found. + /// + /// Column to apply + /// Substring to find + /// Column object + public static Column Instr(Column column, string substring) + { + return ApplyFunction("instr", column, substring); + } + + /// + /// Computes the character length of a given string or number of bytes of a binary string. + /// + /// + /// The length of character strings includes the trailing spaces. The length of binary + /// strings includes binary zeros. + /// + /// Column to apply + /// Column object + public static Column Length(Column column) + { + return ApplyFunction("length", column); + } + + /// + /// Converts a string column to lower case. + /// + /// Column to apply + /// Column object + public static Column Lower(Column column) + { + return ApplyFunction("lower", column); + } + + /// + /// Computes the Levenshtein distance of the two given string columns. + /// + /// Left side column to apply + /// Right side column to apply + /// Column object + public static Column Levenshtein(Column left, Column right) + { + return ApplyFunction("levenshtein", left, right); + } + + /// + /// Locate the position of the first occurrence of the given substring. + /// + /// + /// The position is not zero based, but 1 based index. Returns 0 if the given substring + /// could not be found. + /// + /// Substring to find + /// Column to apply + /// Column object + public static Column Locate(string substring, Column column) + { + return ApplyFunction("locate", substring, column); + } + + /// + /// Locate the position of the first occurrence of the given substring + /// starting from the given position offset. + /// + /// + /// The position is not zero based, but 1 based index. Returns 0 if the given substring + /// could not be found. + /// + /// Substring to find + /// Column to apply + /// Offset to start the search + /// Column object + public static Column Locate(string substring, Column column, int pos) + { + return ApplyFunction("locate", substring, column, pos); + } + + /// + /// Left-pad the string column with pad to the given length `len`. If the string column is + /// longer than `len`, the return value is shortened to `len` characters. + /// + /// Column to apply + /// Length of padded string + /// String used for padding + /// Column object + public static Column Lpad(Column column, int len, string pad) + { + return ApplyFunction("lpad", column, len, pad); + } + + /// + /// Trim the spaces from left end for the given string column. + /// + /// Column to apply + /// Column object + public static Column Ltrim(Column column) + { + return ApplyFunction("ltrim", column); + } + + /// + /// Trim the specified character string from left end for the given string column. + /// + /// Column to apply + /// String to trim + /// Column object + public static Column Ltrim(Column column, string trimString) + { + return ApplyFunction("ltrim", column, trimString); + } + + /// + /// Extract a specific group matched by a Java regex, from the specified string column. + /// + /// + /// If the regex did not match, or the specified group did not match, + /// an empty string is returned. + /// + /// Column to apply + /// Regular expression to match + /// Group index to extract + /// Column object + public static Column RegexpExtract(Column column, string exp, int groupIdx) + { + return ApplyFunction("regexp_extract", column, exp, groupIdx); + } + + /// + /// Replace all substrings of the specified string value that match the pattern with + /// the given replacement string. + /// + /// Column to apply + /// Regular expression to match + /// String to replace with + /// Column object + public static Column RegexpReplace(Column column, string pattern, string replacement) + { + return ApplyFunction("regexp_replace", column, pattern, replacement); + } + + /// + /// Replace all substrings of the specified string value that match the pattern with + /// the given replacement string. + /// + /// Column to apply + /// Regular expression to match + /// String to replace with + /// Column object + public static Column RegexpReplace(Column column, Column pattern, Column replacement) + { + return ApplyFunction("regexp_replace", column, pattern, replacement); + } + + /// + /// Decodes a BASE64 encoded string column and returns it as a binary column. + /// + /// Column to apply + /// Column object + public static Column Unbase64(Column column) + { + return ApplyFunction("unbase64", column); + } + + /// + /// Right-pad the string column with pad to the given length `len`. If the string column is + /// longer than `len`, the return value is shortened to `len` characters. + /// + /// Column to apply + /// Length of padded string + /// String used for padding + /// Column object + public static Column Rpad(Column column, int len, string pad) + { + return ApplyFunction("rpad", column, len, pad); + } + + /// + /// Repeats a string column `n` times, and returns it as a new string column. + /// + /// Column to apply + /// Repeatation number + /// Column object + public static Column Repeat(Column column, int n) + { + return ApplyFunction("repeat", column, n); + } + + /// + /// Trim the spaces from right end for the specified string value. + /// + /// Column to apply + /// Column object + public static Column Rtrim(Column column) + { + return ApplyFunction("rtrim", column); + } + + /// + /// Trim the specified character string from right end for the given string column. + /// + /// Column to apply + /// String to trim + /// Column object + public static Column Rtrim(Column column, string trimString) + { + return ApplyFunction("rtrim", column, trimString); + } + + /// + /// Returns the soundex code for the specified expression. + /// + /// Column to apply + /// Column object + public static Column Soundex(Column column) + { + return ApplyFunction("soundex", column); + } + + /// + /// Splits string with a regular expression pattern. + /// + /// Column to apply + /// Regular expression pattern + /// Column object + public static Column Split(Column column, string pattern) + { + return ApplyFunction("split", column, pattern); + } + + /// + /// Splits str around matches of the given pattern. + /// + /// Column to apply + /// Regular expression pattern + /// An integer expression which controls the number of times the regex + /// is applied. + /// 1. limit greater than 0: The resulting array's length will not be more than limit, and + /// the resulting array's last entry will contain all input beyond the last matched regex. + /// 2. limit less than or equal to 0: `regex` will be applied as many times as possible, + /// and the resulting array can be of any size. + /// + /// Column object + [Since(Versions.V3_0_0)] + public static Column Split(Column column, string pattern, int limit) + { + return ApplyFunction("split", column, pattern, limit); + } + + /// + /// Returns the substring (or slice of byte array) starting from the given + /// position for the given length. + /// + /// + /// The position is not zero based, but 1 based index. + /// + /// Column to apply + /// Starting position + /// Length of the substring + /// Column object + public static Column Substring(Column column, int pos, int len) + { + return ApplyFunction("substring", column, pos, len); + } + + /// + /// Returns the substring from the given string before `count` occurrences of + /// the given delimiter. + /// + /// Column to apply + /// Delimiter to find + /// Number of occurrences of delimiter + /// Column object + public static Column SubstringIndex(Column column, string delimiter, int count) + { + return ApplyFunction("substring_index", column, delimiter, count); + } + + /// + /// Overlay the specified portion of `src` with `replace`, starting from byte position + /// `pos` of `src` and proceeding for `len` bytes. + /// + /// Source column to replace + /// Replacing column + /// Byte position to start overlaying from + /// Number of bytes to overlay + /// Column object + [Since(Versions.V3_0_0)] + public static Column Overlay(Column src, Column replace, Column pos, Column len) + { + return ApplyFunction("overlay", src, replace, pos, len); + } + + /// + /// Overlay the specified portion of `src` with `replace`, starting from byte position + /// `pos` of `src`. + /// + /// Source column to replace + /// Replacing column + /// Byte position to start overlaying from + /// Column object + [Since(Versions.V3_0_0)] + public static Column Overlay(Column src, Column replace, Column pos) + { + return ApplyFunction("overlay", src, replace, pos); + } + + /// + /// Splits a string into arrays of sentences, where each sentence is an array of words. + /// + /// String to split + /// Language of the locale + /// Country of the locale + /// Column object + [Since(Versions.V3_2_0)] + public static Column Sentences(Column str, Column language, Column country) + { + return ApplyFunction("sentences", str, language, country); + } + + /// + /// Splits a string into arrays of sentences, where each sentence is an array of words. + /// The default locale is used. + /// + /// String to split + /// Column object + [Since(Versions.V3_2_0)] + public static Column Sentences(Column str) + { + return ApplyFunction("sentences", str); + } + + /// + /// Translate any characters that match with the given `matchingString` in the column + /// by the given `replaceString`. + /// + /// Column to apply + /// String to match + /// String to replace with + /// Column object + public static Column Translate(Column column, string matchingString, string replaceString) + { + return ApplyFunction("translate", column, matchingString, replaceString); + } + + /// + /// Trim the spaces from both ends for the specified string column. + /// + /// Column to apply + /// Column object + public static Column Trim(Column column) + { + return ApplyFunction("trim", column); + } + + /// + /// Trim the specified character from both ends for the specified string column. + /// + /// Column to apply + /// String to trim + /// Column object + public static Column Trim(Column column, string trimString) + { + return ApplyFunction("trim", column, trimString); + } + + /// + /// Converts a string column to upper case. + /// + /// Column to apply + /// Column object + public static Column Upper(Column column) + { + return ApplyFunction("upper", column); + } + + ///////////////////////////////////////////////////////////////////////////////// + // DateTime functions + ///////////////////////////////////////////////////////////////////////////////// + + /// + /// Returns the date that is `numMonths` after `startDate`. + /// + /// Start date + /// Number of months to add to start date + /// Column object + public static Column AddMonths(Column startDate, int numMonths) + { + return ApplyFunction("add_months", startDate, numMonths); + } + + /// + /// Returns the date that is `numMonths` after `startDate`. + /// + /// Start date + /// A column of the number of months to add to start date + /// Column object + [Since(Versions.V3_0_0)] + public static Column AddMonths(Column startDate, Column numMonths) + { + return ApplyFunction("add_months", startDate, numMonths); + } + + /// + /// Returns the current date as a date column. + /// + /// Column object + public static Column CurrentDate() + { + return ApplyFunction("current_date"); + } + + /// + /// Returns the current timestamp as a timestamp column. + /// + /// Column object + public static Column CurrentTimestamp() + { + return ApplyFunction("current_timestamp"); + } + + /// + /// Converts a date/timestamp/string to a value of string in the format specified + /// by the date format given by the second argument. + /// + /// Date expression + /// Format string to apply + /// Column object + public static Column DateFormat(Column dateExpr, string format) + { + return ApplyFunction("date_format", dateExpr, format); + } + + /// + /// Returns the date that is `days` days after `start`. + /// + /// Start date + /// Number of days to add to start data + /// Column object + public static Column DateAdd(Column start, int days) + { + return ApplyFunction("date_add", start, days); + } + + /// + /// Returns the date that is `days` days after `start`. + /// + /// Start date + /// A column of number of days to add to start data + /// Column object + [Since(Versions.V3_0_0)] + public static Column DateAdd(Column start, Column days) + { + return ApplyFunction("date_add", start, days); + } + + /// + /// Returns the date that is `days` days before `start`. + /// + /// Start date + /// Number of days to subtract from start data + /// Column object + public static Column DateSub(Column start, int days) + { + return ApplyFunction("date_sub", start, days); + } + + /// + /// Returns the date that is `days` days before `start`. + /// + /// Start date + /// A column of number of days to subtract from start data + /// Column object + [Since(Versions.V3_0_0)] + public static Column DateSub(Column start, Column days) + { + return ApplyFunction("date_sub", start, days); + } + + /// + /// Returns the number of days from `start` to `end`. + /// + /// Start date + /// End date + /// Column object + public static Column DateDiff(Column end, Column start) + { + return ApplyFunction("datediff", end, start); + } + + /// + /// Extracts the year as an integer from a given date/timestamp/string. + /// + /// Column to apply + /// Column object + public static Column Year(Column column) + { + return ApplyFunction("year", column); + } + + /// + /// Extracts the quarter as an integer from a given date/timestamp/string. + /// + /// Column to apply + /// Column object + public static Column Quarter(Column column) + { + return ApplyFunction("quarter", column); + } + + /// + /// Extracts the month as an integer from a given date/timestamp/string. + /// + /// Column to apply + /// Column object + public static Column Month(Column column) + { + return ApplyFunction("month", column); + } + + /// + /// Extracts the day of the week as an integer from a given date/timestamp/string. + /// + /// Column to apply + /// Column object + public static Column DayOfWeek(Column column) + { + return ApplyFunction("dayofweek", column); + } + + /// + /// Extracts the day of the month as an integer from a given date/timestamp/string. + /// + /// Column to apply + /// Column object + public static Column DayOfMonth(Column column) + { + return ApplyFunction("dayofmonth", column); + } + + /// + /// Extracts the day of the year as an integer from a given date/timestamp/string. + /// + /// Column to apply + /// Column object + public static Column DayOfYear(Column column) + { + return ApplyFunction("dayofyear", column); + } + + /// + /// Extracts the hours as an integer from a given date/timestamp/string. + /// + /// Column to apply + /// Column object + public static Column Hour(Column column) + { + return ApplyFunction("hour", column); + } + + /// + /// Returns the last day of the month which the given date belongs to. + /// + /// Column to apply + /// Column object + public static Column LastDay(Column column) + { + return ApplyFunction("last_day", column); + } + + /// + /// Extracts the minutes as an integer from a given date/timestamp/string. + /// + /// Column to apply + /// Column object + public static Column Minute(Column column) + { + return ApplyFunction("minute", column); + } + + /// + /// Returns number of months between dates `end` and `stasrt`. + /// + /// Date column + /// Date column + /// Column object + public static Column MonthsBetween(Column end, Column start) + { + return ApplyFunction("months_between", end, start); + } + + /// + /// Returns number of months between dates `end` and `start`. If `roundOff` is set to true, + /// the result is rounded off to 8 digits; it is not rounded otherwise. + /// + /// Date column + /// Date column + /// To round or not + /// Column object + [Since(Versions.V2_4_0)] + public static Column MonthsBetween(Column end, Column start, bool roundOff) + { + return ApplyFunction("months_between", end, start, roundOff); + } + + /// + /// Given a date column, returns the first date which is later than the value of + /// the date column that is on the specified day of the week. + /// + /// Date column + /// + /// One of the following (case-insensitive): + /// "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun". + /// + /// Column object + public static Column NextDay(Column date, string dayOfWeek) + { + return ApplyFunction("next_day", date, dayOfWeek); + } + + /// + /// Given a date column, returns the first date which is later than the value of + /// the date column that is on the specified day of the week. + /// + /// Date column + /// + /// A column of the day of week. One of the following (case-insensitive): + /// "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun". + /// + /// Column object + [Since(Versions.V3_2_0)] + public static Column NextDay(Column date, Column dayOfWeek) + { + return ApplyFunction("next_day", date, dayOfWeek); + } + + /// + /// Extracts the seconds as an integer from a given date/timestamp/string. + /// + /// Column to apply + /// Column object + public static Column Second(Column column) + { + return ApplyFunction("second", column); + } + + /// + /// Extracts the week number as an integer from a given date/timestamp/string. + /// + /// Column to apply + /// Column object + public static Column WeekOfYear(Column column) + { + return ApplyFunction("weekofyear", column); + } + + /// + /// Converts the number of seconds from UNIX epoch (1970-01-01 00:00:00 UTC) to a string + /// representing the timestamp of that moment in the current system time zone with + /// a default format "yyyy-MM-dd HH:mm:ss". + /// + /// Column to apply + /// Column object + public static Column FromUnixTime(Column column) + { + return ApplyFunction("from_unixtime", column); + } + + /// + /// Converts the number of seconds from UNIX epoch (1970-01-01 00:00:00 UTC) to a string + /// representing the timestamp of that moment in the current system time zone with + /// the given format. + /// + /// Column to apply + /// Format of the timestamp + /// Column object + public static Column FromUnixTime(Column column, string format) + { + return ApplyFunction("from_unixtime", column, format); + } + + /// + /// Returns the current Unix timestamp (in seconds). + /// + /// + /// All calls of `UnixTimestamp` within the same query return the same value + /// (i.e. the current timestamp is calculated at the start of query evaluation). + /// + /// Column object + public static Column UnixTimestamp() + { + return ApplyFunction("unix_timestamp"); + } + + /// + /// Converts time string in format yyyy-MM-dd HH:mm:ss to Unix timestamp (in seconds), + /// using the default timezone and the default locale. + /// + /// Column to apply + /// Column object + public static Column UnixTimestamp(Column column) + { + return ApplyFunction("unix_timestamp", column); + } + + /// + /// Converts time string with given format to Unix timestamp (in seconds). + /// + /// + /// Supported date format can be found: + /// http://docs.oracle.com/javase/tutorial/i18n/format/simpleDateFormat.html + /// + /// Column to apply + /// Date format + /// Column object + public static Column UnixTimestamp(Column column, string format) + { + return ApplyFunction("unix_timestamp", column, format); + } + + /// + /// Convert time string to a Unix timestamp (in seconds) by casting rules to + /// `TimestampType`. + /// + /// Column to apply + /// Column object + public static Column ToTimestamp(Column column) + { + return ApplyFunction("to_timestamp", column); + } + + /// + /// Convert time string to a Unix timestamp (in seconds) with specified format. + /// + /// + /// Supported date format can be found: + /// http://docs.oracle.com/javase/tutorial/i18n/format/simpleDateFormat.html + /// + /// Column to apply + /// Date format + /// Column object + public static Column ToTimestamp(Column column, string format) + { + return ApplyFunction("to_timestamp", column, format); + } + + /// + /// Converts the column into `DateType` by casting rules to `DateType`. + /// + /// Column to apply + /// Column object + public static Column ToDate(Column column) + { + return ApplyFunction("to_date", column); + } + + /// + /// Converts the column into a `DateType` with a specified format. + /// + /// + /// Supported date format can be found: + /// http://docs.oracle.com/javase/tutorial/i18n/format/simpleDateFormat.html + /// + /// Column to apply + /// Date format + /// Column object + public static Column ToDate(Column column, string format) + { + return ApplyFunction("to_date", column, format); + } + + /// + /// Returns date truncated to the unit specified by the format. + /// + /// Column to apply + /// + /// 'year', 'yyyy', 'yy' for truncate by year, or + /// 'month', 'mon', 'mm' for truncate by month + /// + /// Column object + public static Column Trunc(Column column, string format) + { + return ApplyFunction("trunc", column, format); + } + + /// + /// Returns timestamp truncated to the unit specified by the format. + /// + /// + /// 'year', 'yyyy', 'yy' for truncate by year, or + /// 'month', 'mon', 'mm' for truncate by month, or + /// 'day', 'dd' for truncate by day, or + /// 'second', 'minute', 'hour', 'week', 'month', 'quarter' + /// + /// Column to apply + /// Column object + public static Column DateTrunc(string format, Column column) + { + return ApplyFunction("date_trunc", format, column); + } + + /// + /// Given a timestamp like '2017-07-14 02:40:00.0', interprets it as a time in UTC, + /// and renders that time as a timestamp in the given time zone. For example, 'GMT+1' + /// would yield '2017-07-14 03:40:00.0'. + /// + /// + /// This API is deprecated in Spark 3.0. + /// + /// Column to apply + /// Timezone string + /// Column object + [Deprecated(Versions.V3_0_0)] + public static Column FromUtcTimestamp(Column column, string tz) + { + return ApplyFunction("from_utc_timestamp", column, tz); + } + + /// + /// Given a timestamp like '2017-07-14 02:40:00.0', interprets it as a time in UTC, + /// and renders that time as a timestamp in the given time zone. For example, 'GMT+1' + /// would yield '2017-07-14 03:40:00.0'. + /// + /// + /// This API is deprecated in Spark 3.0. + /// + /// Column to apply + /// Timezone expression + /// Column object + [Since(Versions.V2_4_0)] + [Deprecated(Versions.V3_0_0)] + public static Column FromUtcTimestamp(Column column, Column tz) + { + return ApplyFunction("from_utc_timestamp", column, tz); + } + + /// + /// Given a timestamp like '2017-07-14 02:40:00.0', interprets it as a time in the + /// given time zone, and renders that time as a timestamp in UTC. For example, 'GMT+1' + /// would yield '2017-07-14 01:40:00.0'. + /// + /// + /// This API is deprecated in Spark 3.0. + /// + /// Column to apply + /// Timezone string + /// Column object + [Deprecated(Versions.V3_0_0)] + public static Column ToUtcTimestamp(Column column, string tz) + { + return ApplyFunction("to_utc_timestamp", column, tz); + } + + /// + /// Given a timestamp like '2017-07-14 02:40:00.0', interprets it as a time in the + /// given time zone, and renders that time as a timestamp in UTC. For example, 'GMT+1' + /// would yield '2017-07-14 01:40:00.0'. + /// + /// + /// This API is deprecated in Spark 3.0. + /// + /// Column to apply + /// Timezone expression + /// Column object + [Since(Versions.V2_4_0)] + [Deprecated(Versions.V3_0_0)] + public static Column ToUtcTimestamp(Column column, Column tz) + { + return ApplyFunction("to_utc_timestamp", column, tz); + } + + /// + /// Bucketize rows into one or more time windows given a timestamp column. + /// + /// + /// Refer to org.apache.spark.unsafe.types.CalendarInterval for the duration strings. + /// + /// The column to use as the timestamp for windowing by time + /// A string specifying the width of the window + /// + /// A string specifying the sliding interval of the window + /// + /// + /// The offset with respect to 1970-01-01 00:00:00 UTC with which to start window intervals + /// + /// Column object + public static Column Window( + Column column, + string windowDuration, + string slideDuration, + string startTime) + { + return ApplyFunction("window", column, windowDuration, slideDuration, startTime); + } + + /// + /// Bucketize rows into one or more time windows given a timestamp column. + /// + /// + /// Refer to org.apache.spark.unsafe.types.CalendarInterval for the duration strings. + /// + /// The column to use as the timestamp for windowing by time + /// A string specifying the width of the window + /// + /// A string specifying the sliding interval of the window + /// + /// Column object + public static Column Window( + Column column, + string windowDuration, + string slideDuration) + { + return ApplyFunction("window", column, windowDuration, slideDuration); + } + + /// + /// Generates tumbling time windows given a timestamp specifying column. + /// + /// The column to use as the timestamp for windowing by time + /// + /// A string specifying the width of the window. + /// Refer to org.apache.spark.unsafe.types.CalendarInterval for the duration strings. + /// + /// Column object + public static Column Window(Column column, string windowDuration) + { + return ApplyFunction("window", column, windowDuration); + } + + /// + /// Generates session window given a timestamp specifying column. + /// + /// The column or the expression to use as the timestamp for windowing by + /// time + /// A string specifying the timeout of the session, e.g. '10 minutes', + /// '1 second' + /// Column object + [Since(Versions.V3_2_0)] + public static Column Session_Window(Column timeColumn, string gapDuration) + { + return ApplyFunction("session_window", timeColumn, gapDuration); + } + + /// + /// Generates session window given a timestamp specifying column. + /// + /// The column or the expression to use as the timestamp for windowing by + /// time + /// A column specifying the timeout of the session. It could be static + /// value, e.g. `10 minutes`, `1 second`, or an expression/UDF that specifies gap duration + /// dynamically based on the input row. + /// Column object + [Since(Versions.V3_2_0)] + public static Column Session_Window(Column timeColumn, Column gapDuration) + { + return ApplyFunction("session_window", timeColumn, gapDuration); + } + + /// + /// Creates timestamp from the number of seconds since UTC epoch. + /// + /// Column to apply + /// Column object + [Since(Versions.V3_1_0)] + public static Column TimestampSeconds(Column column) + { + return ApplyFunction("timestamp_seconds", column); + } + + ///////////////////////////////////////////////////////////////////////////////// + // Collection functions + ///////////////////////////////////////////////////////////////////////////////// + + /// + /// Returns null if the array is null, true if the array contains `value`, + /// and false otherwise. + /// + /// Column to apply + /// Value to check for existence + /// Column object + public static Column ArrayContains(Column column, object value) + { + return ApplyFunction("array_contains", column, value); + } + + /// + /// Returns true if `a1` and `a2` have at least one non-null element in common. + /// If not and both arrays are non-empty and any of them contains a null, + /// it returns null. It returns false otherwise. + /// + /// Left side array + /// Right side array + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArraysOverlap(Column a1, Column a2) + { + return ApplyFunction("arrays_overlap", a1, a2); + } + + /// + /// Returns an array containing all the elements in `column` from index `start` + /// (or starting from the end if `start` is negative) with the specified `length`. + /// + /// Column to apply + /// Start position in the array + /// Length for slicing + /// Column object + [Since(Versions.V2_4_0)] + public static Column Slice(Column column, int start, int length) + { + return ApplyFunction("slice", column, start, length); + } + + /// + /// Returns an array containing all the elements in `column` from index `start` + /// (or starting from the end if `start` is negative) with the specified `length`. + /// + /// Column to apply + /// Start position in the array + /// Length for slicing + /// Column object + [Since(Versions.V3_1_0)] + public static Column Slice(Column column, Column start, Column length) + { + return ApplyFunction("slice", column, start, length); + } + + /// + /// Concatenates the elements of `column` using the `delimiter`. + /// Null values are replaced with `nullReplacement`. + /// + /// Column to apply + /// Delimiter for join + /// String to replace null value + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayJoin(Column column, string delimiter, string nullReplacement) + { + return ApplyFunction("array_join", column, delimiter, nullReplacement); + } + + /// + /// Concatenates the elements of `column` using the `delimiter`. + /// + /// Column to apply + /// Delimiter for join + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayJoin(Column column, string delimiter) + { + return ApplyFunction("array_join", column, delimiter); + } + + /// + /// Concatenates multiple input columns together into a single column. + /// + /// + /// If all inputs are binary, concat returns an output as binary. + /// Otherwise, it returns as string. + /// + /// Columns to apply + /// Column object + public static Column Concat(params Column[] columns) + { + return ApplyFunction("concat", (object)columns); + } + + /// + /// Locates the position of the first occurrence of the value in the given array as long. + /// Returns null if either of the arguments are null. + /// + /// + /// The position is not zero based, but 1 based index. + /// Returns 0 if value could not be found in array. + /// + /// Column to apply + /// Value to locate + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayPosition(Column column, object value) + { + return ApplyFunction("array_position", column, value); + } + + /// + /// Returns element of array at given index in `value` if column is array. + /// Returns value for the given key in `value` if column is map. + /// + /// Column to apply + /// Value to locate + /// Column object + [Since(Versions.V2_4_0)] + public static Column ElementAt(Column column, object value) + { + return ApplyFunction("element_at", column, value); + } + + /// + /// Sorts the input array in ascending order. The elements of the input array must + /// be sortable. Null elements will be placed at the end of the returned array. + /// + /// Column to apply + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArraySort(Column column) + { + return ApplyFunction("array_sort", column); + } + + /// + /// Remove all elements that equal to element from the given array. + /// + /// Column to apply + /// Element to remove + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayRemove(Column column, object element) + { + return ApplyFunction("array_remove", column, element); + } + + /// + /// Removes duplicate values from the array. + /// + /// Column to apply + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayDistinct(Column column) + { + return ApplyFunction("array_distinct", column); + } + + /// + /// Returns an array of the elements in the intersection of the given two arrays, + /// without duplicates. + /// s + /// Left side column to apply + /// Right side column to apply + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayIntersect(Column col1, Column col2) + { + return ApplyFunction("array_intersect", col1, col2); + } + + /// + /// Returns an array of the elements in the union of the given two arrays, + /// without duplicates. + /// + /// Left side column to apply + /// Right side column to apply + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayUnion(Column col1, Column col2) + { + return ApplyFunction("array_union", col1, col2); + } + + /// + /// Returns an array of the elements in the `col1` but not in the `col2`, + /// without duplicates. The order of elements in the result is nondeterministic. + /// + /// Left side column to apply + /// Right side column to apply + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayExcept(Column col1, Column col2) + { + return ApplyFunction("array_except", col1, col2); + } + + /// + /// Creates a new row for each element in the given array or map column. + /// + /// Column to apply + /// Column object + public static Column Explode(Column column) + { + return ApplyFunction("explode", column); + } + + /// + /// Creates a new row for each element in the given array or map column. + /// Unlike Explode(), if the array/map is null or empty then null is produced. + /// + /// Column to apply + /// Column object + public static Column ExplodeOuter(Column column) + { + return ApplyFunction("explode_outer", column); + } + + /// + /// Creates a new row for each element with position in the given array or map column. + /// + /// Column to apply + /// Column object + public static Column PosExplode(Column column) + { + return ApplyFunction("posexplode", column); + } + + /// + /// Creates a new row for each element with position in the given array or map column. + /// Unlike Posexplode(), if the array/map is null or empty then the row(null, null) + /// is produced. + /// + /// Column to apply + /// Column object + public static Column PosExplodeOuter(Column column) + { + return ApplyFunction("posexplode_outer", column); + } + + /// + /// Extracts JSON object from a JSON string based on path specified, and returns JSON + /// string of the extracted JSON object. + /// + /// Column to apply + /// JSON file path + /// Column object + public static Column GetJsonObject(Column column, string path) + { + return ApplyFunction("get_json_object", column, path); + } + + /// + /// Creates a new row for a JSON column according to the given field names. + /// + /// Column to apply + /// Field names + /// Column object + public static Column JsonTuple(Column column, params string[] fields) + { + return ApplyFunction("json_tuple", column, fields); + } + + /// + /// Parses a column containing a JSON string into a `StructType` or `ArrayType` + /// of `StructType`s with the specified schema. + /// + /// Column to apply + /// JSON format string or DDL-formatted string for a schema + /// Options for JSON parsing + /// Column object + public static Column FromJson( + Column column, + string schema, + Dictionary options = null) + { + return ApplyFunction( + "from_json", + column, + schema, + options ?? new Dictionary()); + } + + /// + /// Parses a column containing a JSON string into a `StructType` or `ArrayType` + /// of `StructType`s with the specified schema. + /// + /// String column containing JSON data + /// The schema to use when parsing the JSON string + /// Options for JSON parsing + /// Column object + [Since(Versions.V2_4_0)] + public static Column FromJson( + Column column, + Column schema, + Dictionary options = null) + { + return ApplyFunction( + "from_json", + column, + schema, + options ?? new Dictionary()); + } + + /// + /// Parses a JSON string and infers its schema in DDL format. + /// + /// JSON string + /// Column object + [Since(Versions.V2_4_0)] + public static Column SchemaOfJson(string json) + { + return ApplyFunction("schema_of_json", json); + } + + /// + /// Parses a JSON string and infers its schema in DDL format. + /// + /// String literal containing a JSON string. + /// Column object + [Since(Versions.V2_4_0)] + public static Column SchemaOfJson(Column json) + { + return ApplyFunction("schema_of_json", json); + } + + /// + /// Parses a JSON string and infers its schema in DDL format. + /// + /// String literal containing a JSON string. + /// Options to control how the json is parsed. + /// Column object + [Since(Versions.V3_0_0)] + public static Column SchemaOfJson(Column json, Dictionary options) + { + return ApplyFunction("schema_of_json", json, options); + } + + /// + /// Converts a column containing a `StructType`, `ArrayType` of `StructType`s, + /// a `MapType` or `ArrayType` of `MapType`s into a JSON string. + /// + /// Column to apply + /// Options for JSON conversion + /// Column object + public static Column ToJson( + Column column, + Dictionary options = null) + { + return ApplyFunction( + "to_json", + column, + options ?? new Dictionary()); + } + + /// + /// Returns length of array or map. + /// + /// Column to apply + /// Column object + public static Column Size(Column column) + { + return ApplyFunction("size", column); + } + + /// + /// Sorts the input array for the given column in ascending (default) or + /// descending order, the natural ordering of the array elements. + /// + /// Column to apply + /// True for ascending order and false for descending order + /// Column object + public static Column SortArray(Column column, bool asc = true) + { + return ApplyFunction("sort_array", column, asc); + } + + /// + /// Returns the minimum value in the array. + /// + /// Column to apply + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayMin(Column column) + { + return ApplyFunction("array_min", column); + } + + /// + /// Returns the maximum value in the array. + /// + /// Column to apply + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayMax(Column column) + { + return ApplyFunction("array_max", column); + } + + /// + /// Returns a random permutation of the given array. + /// + /// Column to apply + /// Column object + [Since(Versions.V2_4_0)] + public static Column Shuffle(Column column) + { + return ApplyFunction("shuffle", column); + } + + /// + /// Reverses the string column and returns it as a new string column. + /// + /// Column to apply + /// Column object + public static Column Reverse(Column column) + { + return ApplyFunction("reverse", column); + } + + /// + /// Creates a single array from an array of arrays. If a structure of nested arrays + /// is deeper than two levels, only one level of nesting is removed. + /// + /// Column to apply + /// Column object + [Since(Versions.V2_4_0)] + public static Column Flatten(Column column) + { + return ApplyFunction("flatten", column); + } + + /// + /// Generate a sequence of integers from `start` to `stop`, incrementing by `step`. + /// + /// Start expression + /// Stop expression + /// Step to increment + /// Column object + [Since(Versions.V2_4_0)] + public static Column Sequence(Column start, Column stop, Column step) + { + return ApplyFunction("sequence", start, stop, step); + } + + /// + /// Generate a sequence of integers from start to stop, incrementing by 1 if start is + /// less than or equal to stop, otherwise -1. + /// + /// Start expression + /// Stop expression + /// Column object + [Since(Versions.V2_4_0)] + public static Column Sequence(Column start, Column stop) + { + return ApplyFunction("sequence", start, stop); + } + + /// + /// Creates an array containing the `left` argument repeated the number of times given by + /// the `right` argument. + /// + /// Left column expression + /// Right column expression + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayRepeat(Column left, Column right) + { + return ApplyFunction("array_repeat", left, right); + } + + /// + /// Creates an array containing the `left` argument repeated the `count` number of times. + /// + /// Left column expression + /// Number of times to repeat + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArrayRepeat(Column left, int count) + { + return ApplyFunction("array_repeat", left, count); + } + + /// + /// Returns an unordered array containing the keys of the map. + /// + /// Column to apply + /// Column object + public static Column MapKeys(Column column) + { + return ApplyFunction("map_keys", column); + } + + /// + /// Returns an unordered array containing the values of the map. + /// + /// Column to apply + /// Column object + public static Column MapValues(Column column) + { + return ApplyFunction("map_values", column); + } + + /// + /// Returns an unordered array of all entries in the given map. + /// + /// Column to apply + /// Column object + [Since(Versions.V3_0_0)] + public static Column MapEntries(Column column) + { + return ApplyFunction("map_entries", column); + } + + /// + /// Returns a map created from the given array of entries. + /// + /// Column to apply + /// Column object + [Since(Versions.V2_4_0)] + public static Column MapFromEntries(Column column) + { + return ApplyFunction("map_from_entries", column); + } + + /// + /// Returns a merged array of structs in which the N-th struct contains all + /// N-th values of input arrays. + /// + /// Columns to zip + /// Column object + [Since(Versions.V2_4_0)] + public static Column ArraysZip(params Column[] columns) + { + return ApplyFunction("arrays_zip", (object)columns); + } + + /// + /// Returns the union of all the given maps. + /// + /// Columns to apply + /// Column object + [Since(Versions.V2_4_0)] + public static Column MapConcat(params Column[] columns) + { + return ApplyFunction("map_concat", (object)columns); + } + + /// + /// Parses a column containing a CSV string into a `StructType` with the specified schema. + /// + /// Column to apply + /// The schema to use when parsing the CSV string + /// Options to control how the CSV is parsed. + /// Column object + [Since(Versions.V3_0_0)] + public static Column FromCsv( + Column column, + StructType schema, + Dictionary options) + { + return ApplyFunction( + "from_csv", + column, + DataType.FromJson(Jvm, schema.Json), + options); + } + + /// + /// Parses a column containing a CSV string into a `StructType` with the specified schema. + /// + /// Column to apply + /// The schema to use when parsing the CSV string + /// Options to control how the CSV is parsed. + /// Column object + [Since(Versions.V3_0_0)] + public static Column FromCsv( + Column column, + Column schema, + Dictionary options) + { + return ApplyFunction("from_csv", column, schema, options); + } + + /// + /// Parses a CSV string and infers its schema in DDL format. + /// + /// CSV string to parse + /// Column object + [Since(Versions.V3_0_0)] + public static Column SchemaOfCsv(string csv) + { + return ApplyFunction("schema_of_csv", csv); + } + + /// + /// Parses a CSV string and infers its schema in DDL format. + /// + /// CSV string to parse + /// Column object + [Since(Versions.V3_0_0)] + public static Column SchemaOfCsv(Column csv) + { + return ApplyFunction("schema_of_csv", csv); + } + + /// + /// Parses a CSV string and infers its schema in DDL format. + /// + /// CSV string to parse + /// Options to control how the CSV is parsed. + /// Column object + [Since(Versions.V3_0_0)] + public static Column SchemaOfCsv(Column csv, Dictionary options) + { + return ApplyFunction("schema_of_csv", csv, options); + } + + /// + /// Converts a column containing a `StructType` into a CSV string with the specified + /// schema. + /// + /// A column containing a struct. + /// Column object + [Since(Versions.V3_0_0)] + public static Column ToCsv(Column column) + { + return ApplyFunction("to_csv", column); + } + + /// + /// Converts a column containing a `StructType` into a CSV string with the specified + /// schema. + /// + /// A column containing a struct. + /// Options to control how the struct column is converted into a CSV + /// string + /// Column object + [Since(Versions.V3_0_0)] + public static Column ToCsv(Column column, Dictionary options) + { + return ApplyFunction("to_csv", column, options); + } + + /// + /// A transform for timestamps and dates to partition data into years. + /// + /// A column containing a struct. + /// Column object + [Since(Versions.V3_0_0)] + public static Column Years(Column column) + { + return ApplyFunction("years", column); + } + + /// + /// A transform for timestamps and dates to partition data into months. + /// + /// A column containing a struct. + /// Column object + [Since(Versions.V3_0_0)] + public static Column Months(Column column) + { + return ApplyFunction("months", column); + } + + /// + /// A transform for timestamps and dates to partition data into days. + /// + /// A column containing a struct. + /// Column object + [Since(Versions.V3_0_0)] + public static Column Days(Column column) + { + return ApplyFunction("days", column); + } + + /// + /// A transform for timestamps to partition data into hours. + /// + /// A column containing a struct. + /// Column object + [Since(Versions.V3_0_0)] + public static Column Hours(Column column) + { + return ApplyFunction("hours", column); + } + + /// + /// A transform for any type that partitions by a hash of the input column. + /// + /// A column containing number of buckets + /// Column to apply + /// Column object + [Since(Versions.V3_0_0)] + public static Column Bucket(Column numBuckets, Column column) + { + return ApplyFunction("bucket", numBuckets, column); + } + + /// + /// A transform for any type that partitions by a hash of the input column. + /// + /// Number of buckets + /// Column to apply + /// Column object + [Since(Versions.V3_0_0)] + public static Column Bucket(int numBuckets, Column column) + { + return ApplyFunction("bucket", numBuckets, column); + } + + ///////////////////////////////////////////////////////////////////////////////// + // UDF helper functions + ///////////////////////////////////////////////////////////////////////////////// + + /// Creates a UDF from the specified delegate. + /// Specifies the return type of the UDF. + /// The UDF function implementation. + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf(Func udf) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf)).Apply0; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF function implementation. + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf(Func udf) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf)).Apply1; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF function implementation. + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf(Func udf) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf)).Apply2; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF function implementation. + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf)).Apply3; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF function implementation. + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf)).Apply4; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF function implementation. + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf)).Apply5; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF function implementation. + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf)).Apply6; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF function implementation. + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf)).Apply7; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF function implementation. + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf)).Apply8; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF function implementation. + /// A delegate that when invoked will return a for the result of the UDF. + public static Func Udf( + Func udf) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf)).Apply9; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the type of the tenth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF function implementation. + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf)).Apply10; + } + + /// Creates a UDF from the specified delegate. + /// The UDF function implementation. + /// Schema associated with this row + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf(Func udf, StructType returnType) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf), returnType).Apply0; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// The UDF function implementation. + /// Schema associated with this row + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf(Func udf, StructType returnType) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf), returnType).Apply1; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// The UDF function implementation. + /// Schema associated with this row + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf, StructType returnType) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf), returnType).Apply2; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// The UDF function implementation. + /// Schema associated with this row + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf, StructType returnType) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf), returnType).Apply3; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// The UDF function implementation. + /// Schema associated with this row + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf, StructType returnType) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf), returnType).Apply4; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// The UDF function implementation. + /// Schema associated with this row + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf, StructType returnType) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf), returnType).Apply5; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// The UDF function implementation. + /// Schema associated with this row + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf, StructType returnType) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf), returnType).Apply6; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// The UDF function implementation. + /// Schema associated with this row + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf, StructType returnType) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf), returnType).Apply7; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// The UDF function implementation. + /// Schema associated with this row + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf, StructType returnType) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf), returnType).Apply8; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// The UDF function implementation. + /// Schema associated with this row + /// A delegate that when invoked will return a for the result of the UDF. + public static Func Udf( + Func udf, StructType returnType) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf), returnType).Apply9; + } + + /// Creates a UDF from the specified delegate. + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the type of the tenth argument to the UDF. + /// The UDF function implementation. + /// Schema associated with this row + /// + /// A delegate that returns a for the result of the UDF. + /// + public static Func Udf( + Func udf, StructType returnType) + { + return CreateUdf(udf.Method.ToString(), UdfUtils.CreateUdfWrapper(udf), returnType).Apply10; + } + + /// + /// Call a user-defined function registered via SparkSession.Udf().Register(). + /// + /// Name of the registered UDF + /// Columns to apply + /// Column object + [Deprecated(Versions.V3_2_0)] + public static Column CallUDF(string udfName, params Column[] columns) + { + return ApplyFunction("callUDF", udfName, columns); + } + + /// + /// Call a user-defined function registered via SparkSession.Udf().Register(). + /// + /// Name of the registered UDF + /// Columns to apply + /// Column object + [Since(Versions.V3_2_0)] + public static Column Call_UDF(string udfName, params Column[] columns) + { + return ApplyFunction("call_udf", udfName, columns); + } + + private static UserDefinedFunction CreateUdf(string name, Delegate execute) + { + return CreateUdf(name, execute, UdfUtils.PythonEvalType.SQL_BATCHED_UDF); + } + + private static UserDefinedFunction CreateUdf(string name, Delegate execute, StructType returnType) + { + return CreateUdf(name, execute, UdfUtils.PythonEvalType.SQL_BATCHED_UDF, returnType); + } + + internal static UserDefinedFunction CreateVectorUdf(string name, Delegate execute) + { + return CreateUdf(name, execute, UdfUtils.PythonEvalType.SQL_SCALAR_PANDAS_UDF); + } + + private static UserDefinedFunction CreateUdf( + string name, + Delegate execute, + UdfUtils.PythonEvalType evalType) => + CreateUdf(name, execute, evalType, UdfUtils.GetReturnType(typeof(TResult))); + + private static UserDefinedFunction CreateUdf( + string name, + Delegate execute, + UdfUtils.PythonEvalType evalType, + StructType returnType) => + CreateUdf(name, execute, evalType, returnType.Json); + + private static UserDefinedFunction CreateUdf( + string name, + Delegate execute, + UdfUtils.PythonEvalType evalType, + string returnType) + { + return UserDefinedFunction.Create( + name, + CommandSerDe.Serialize( + execute, + CommandSerDe.SerializedMode.Row, + CommandSerDe.SerializedMode.Row), + evalType, + returnType); + } + + private static Column ApplyFunction(string funcName) + { + return new Column( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_functionsClassName, + funcName)); + } + + private static Column ApplyFunction(string funcName, object arg) + { + return new Column( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_functionsClassName, + funcName, + arg)); + } + + private static Column ApplyFunction(string funcName, object arg1, object arg2) + { + return new Column( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_functionsClassName, + funcName, + arg1, + arg2)); + } + + private static Column ApplyFunction(string funcName, params object[] args) + { + return new Column( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_functionsClassName, + funcName, + args)); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/GenericRow.cs b/src/spark/Flowthru.Spark/Sql/GenericRow.cs new file mode 100644 index 00000000..4e31b02f --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/GenericRow.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Flowthru.Spark.Sql +{ + /// + /// Represents a row object in RDD, equivalent to GenericRow in Spark. + /// + public sealed class GenericRow + { + /// + /// Constructor for the GenericRow class. + /// + /// Column values for a row + public GenericRow(object[] values) + { + Values = values; + } + + /// + /// Values representing this row. + /// + public object[] Values { get; } + + /// + /// Returns the number of columns in this row. + /// + /// Number of columns in this row + public int Size() => Values.Length; + + /// + /// Returns the column value at the given index. + /// + /// Index to look up + /// A column value + public object this[int index] => Get(index); + + /// + /// Returns the column value at the given index. + /// + /// Index to look up + /// A column value + public object Get(int index) + { + if (index >= Size()) + { + throw new IndexOutOfRangeException($"index ({index}) >= column counts ({Size()})"); + } + else if (index < 0) + { + throw new IndexOutOfRangeException($"index ({index}) < 0)"); + } + + return Values[index]; + } + + /// + /// Returns the string version of this row. + /// + /// String version of this row + public override string ToString() + { + var cols = new List(); + foreach (object item in Values) + { + cols.Add(item?.ToString() ?? string.Empty); + } + + return $"[{(string.Join(",", cols.ToArray()))}]"; + } + + /// + /// Returns the column value at the given index, as a type T. + /// TODO: If the original type is "long" and its value can be + /// fit into the "int", Pickler will serialize the value as int. + /// Since the value is boxed, will throw an exception. + /// + /// Type to convert to + /// Index to look up + /// A column value as a type T + public T GetAs(int index) => (T)Get(index); + + /// + /// Checks if the given object is same as the current object. + /// + /// Other object to compare against + /// True if the other object is equal. + public override bool Equals(object obj) => + ReferenceEquals(this, obj) || + ((obj is GenericRow row) && Values.SequenceEqual(row.Values)); + + /// + /// Returns the hash code of the current object. + /// + /// The hash code of the current object + public override int GetHashCode() => base.GetHashCode(); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/PicklingUdfWrapper.cs b/src/spark/Flowthru.Spark/Sql/PicklingUdfWrapper.cs new file mode 100644 index 00000000..b2a27bd2 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/PicklingUdfWrapper.cs @@ -0,0 +1,422 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using static Flowthru.Spark.Utils.TypeConverter; + +namespace Flowthru.Spark.Sql +{ + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the return type of the UDF. + [UdfWrapper] + internal class PicklingUdfWrapper + { + private readonly Func _func; + + internal PicklingUdfWrapper(Func func) + { + _func = func; + } + + internal object Execute(int splitIndex, object[] input, int[] argOffsets) + { + return _func(); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal class PicklingUdfWrapper + { + [NonSerialized] + private bool? _sameT = null; + + private readonly Func _func; + + internal PicklingUdfWrapper(Func func) + { + _func = func; + } + + internal object Execute(int splitIndex, object[] input, int[] argOffsets) + { + object param = input[argOffsets[0]]; + return _func((_sameT ??= param is T) ? (T)param : ConvertTo(param)); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal class PicklingUdfWrapper + { + [NonSerialized] + private readonly bool?[] _sameT = new bool?[2]; + + private readonly Func _func; + + internal PicklingUdfWrapper(Func func) + { + _func = func; + } + + internal object Execute(int splitIndex, object[] input, int[] argOffsets) + { + object param1 = input[argOffsets[0]]; + object param2 = input[argOffsets[1]]; + return _func( + (_sameT[0] ??= param1 is T1) ? (T1)param1 : ConvertTo(param1), + (_sameT[1] ??= param2 is T2) ? (T2)param2 : ConvertTo(param2)); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal class PicklingUdfWrapper + { + [NonSerialized] + private readonly bool?[] _sameT = new bool?[3]; + + private readonly Func _func; + + internal PicklingUdfWrapper(Func func) + { + _func = func; + } + + internal object Execute(int splitIndex, object[] input, int[] argOffsets) + { + object param1 = input[argOffsets[0]]; + object param2 = input[argOffsets[1]]; + object param3 = input[argOffsets[2]]; + return _func( + (_sameT[0] ??= param1 is T1) ? (T1)param1 : ConvertTo(param1), + (_sameT[1] ??= param2 is T2) ? (T2)param2 : ConvertTo(param2), + (_sameT[2] ??= param3 is T3) ? (T3)param3 : ConvertTo(param3)); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal class PicklingUdfWrapper + { + [NonSerialized] + private readonly bool?[] _sameT = new bool?[4]; + + private readonly Func _func; + + internal PicklingUdfWrapper(Func func) + { + _func = func; + } + + internal object Execute(int splitIndex, object[] input, int[] argOffsets) + { + object param1 = input[argOffsets[0]]; + object param2 = input[argOffsets[1]]; + object param3 = input[argOffsets[2]]; + object param4 = input[argOffsets[3]]; + return _func( + (_sameT[0] ??= param1 is T1) ? (T1)param1 : ConvertTo(param1), + (_sameT[1] ??= param2 is T2) ? (T2)param2 : ConvertTo(param2), + (_sameT[2] ??= param3 is T3) ? (T3)param3 : ConvertTo(param3), + (_sameT[3] ??= param4 is T4) ? (T4)param4 : ConvertTo(param4)); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal class PicklingUdfWrapper + { + [NonSerialized] + private readonly bool?[] _sameT = new bool?[5]; + + private readonly Func _func; + + internal PicklingUdfWrapper(Func func) + { + _func = func; + } + + internal object Execute(int splitIndex, object[] input, int[] argOffsets) + { + object param1 = input[argOffsets[0]]; + object param2 = input[argOffsets[1]]; + object param3 = input[argOffsets[2]]; + object param4 = input[argOffsets[3]]; + object param5 = input[argOffsets[4]]; + return _func( + (_sameT[0] ??= param1 is T1) ? (T1)param1 : ConvertTo(param1), + (_sameT[1] ??= param2 is T2) ? (T2)param2 : ConvertTo(param2), + (_sameT[2] ??= param3 is T3) ? (T3)param3 : ConvertTo(param3), + (_sameT[3] ??= param4 is T4) ? (T4)param4 : ConvertTo(param4), + (_sameT[4] ??= param5 is T5) ? (T5)param5 : ConvertTo(param5)); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal class PicklingUdfWrapper + { + [NonSerialized] + private readonly bool?[] _sameT = new bool?[6]; + + private readonly Func _func; + + internal PicklingUdfWrapper(Func func) + { + _func = func; + } + + internal object Execute(int splitIndex, object[] input, int[] argOffsets) + { + object param1 = input[argOffsets[0]]; + object param2 = input[argOffsets[1]]; + object param3 = input[argOffsets[2]]; + object param4 = input[argOffsets[3]]; + object param5 = input[argOffsets[4]]; + object param6 = input[argOffsets[5]]; + return _func( + (_sameT[0] ??= param1 is T1) ? (T1)param1 : ConvertTo(param1), + (_sameT[1] ??= param2 is T2) ? (T2)param2 : ConvertTo(param2), + (_sameT[2] ??= param3 is T3) ? (T3)param3 : ConvertTo(param3), + (_sameT[3] ??= param4 is T4) ? (T4)param4 : ConvertTo(param4), + (_sameT[4] ??= param5 is T5) ? (T5)param5 : ConvertTo(param5), + (_sameT[5] ??= param6 is T6) ? (T6)param6 : ConvertTo(param6)); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal class PicklingUdfWrapper + { + [NonSerialized] + private readonly bool?[] _sameT = new bool?[7]; + + private readonly Func _func; + + internal PicklingUdfWrapper(Func func) + { + _func = func; + } + + internal object Execute(int splitIndex, object[] input, int[] argOffsets) + { + object param1 = input[argOffsets[0]]; + object param2 = input[argOffsets[1]]; + object param3 = input[argOffsets[2]]; + object param4 = input[argOffsets[3]]; + object param5 = input[argOffsets[4]]; + object param6 = input[argOffsets[5]]; + object param7 = input[argOffsets[6]]; + return _func( + (_sameT[0] ??= param1 is T1) ? (T1)param1 : ConvertTo(param1), + (_sameT[1] ??= param2 is T2) ? (T2)param2 : ConvertTo(param2), + (_sameT[2] ??= param3 is T3) ? (T3)param3 : ConvertTo(param3), + (_sameT[3] ??= param4 is T4) ? (T4)param4 : ConvertTo(param4), + (_sameT[4] ??= param5 is T5) ? (T5)param5 : ConvertTo(param5), + (_sameT[5] ??= param6 is T6) ? (T6)param6 : ConvertTo(param6), + (_sameT[6] ??= param7 is T7) ? (T7)param7 : ConvertTo(param7)); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal class PicklingUdfWrapper + { + [NonSerialized] + private readonly bool?[] _sameT = new bool?[8]; + + private readonly Func _func; + + internal PicklingUdfWrapper(Func func) + { + _func = func; + } + + internal object Execute(int splitIndex, object[] input, int[] argOffsets) + { + object param1 = input[argOffsets[0]]; + object param2 = input[argOffsets[1]]; + object param3 = input[argOffsets[2]]; + object param4 = input[argOffsets[3]]; + object param5 = input[argOffsets[4]]; + object param6 = input[argOffsets[5]]; + object param7 = input[argOffsets[6]]; + object param8 = input[argOffsets[7]]; + return _func( + (_sameT[0] ??= param1 is T1) ? (T1)param1 : ConvertTo(param1), + (_sameT[1] ??= param2 is T2) ? (T2)param2 : ConvertTo(param2), + (_sameT[2] ??= param3 is T3) ? (T3)param3 : ConvertTo(param3), + (_sameT[3] ??= param4 is T4) ? (T4)param4 : ConvertTo(param4), + (_sameT[4] ??= param5 is T5) ? (T5)param5 : ConvertTo(param5), + (_sameT[5] ??= param6 is T6) ? (T6)param6 : ConvertTo(param6), + (_sameT[6] ??= param7 is T7) ? (T7)param7 : ConvertTo(param7), + (_sameT[7] ??= param8 is T8) ? (T8)param8 : ConvertTo(param8)); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal class PicklingUdfWrapper + { + [NonSerialized] + private readonly bool?[] _sameT = new bool?[9]; + + private readonly Func _func; + + internal PicklingUdfWrapper(Func func) + { + _func = func; + } + internal object Execute(int splitIndex, object[] input, int[] argOffsets) + { + object param1 = input[argOffsets[0]]; + object param2 = input[argOffsets[1]]; + object param3 = input[argOffsets[2]]; + object param4 = input[argOffsets[3]]; + object param5 = input[argOffsets[4]]; + object param6 = input[argOffsets[5]]; + object param7 = input[argOffsets[6]]; + object param8 = input[argOffsets[7]]; + object param9 = input[argOffsets[8]]; + return _func( + (_sameT[0] ??= param1 is T1) ? (T1)param1 : ConvertTo(param1), + (_sameT[1] ??= param2 is T2) ? (T2)param2 : ConvertTo(param2), + (_sameT[2] ??= param3 is T3) ? (T3)param3 : ConvertTo(param3), + (_sameT[3] ??= param4 is T4) ? (T4)param4 : ConvertTo(param4), + (_sameT[4] ??= param5 is T5) ? (T5)param5 : ConvertTo(param5), + (_sameT[5] ??= param6 is T6) ? (T6)param6 : ConvertTo(param6), + (_sameT[6] ??= param7 is T7) ? (T7)param7 : ConvertTo(param7), + (_sameT[7] ??= param8 is T8) ? (T8)param8 : ConvertTo(param8), + (_sameT[8] ??= param9 is T9) ? (T9)param9 : ConvertTo(param9)); + } + } + + /// + /// Wraps the given Func object, which represents a UDF. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the type of the tenth argument to the UDF. + /// Specifies the return type of the UDF. + [UdfWrapper] + internal class PicklingUdfWrapper + { + [NonSerialized] + private readonly bool?[] _sameT = new bool?[10]; + + private readonly Func _func; + + internal PicklingUdfWrapper(Func func) + { + _func = func; + } + + internal object Execute(int splitIndex, object[] input, int[] argOffsets) + { + object param1 = input[argOffsets[0]]; + object param2 = input[argOffsets[1]]; + object param3 = input[argOffsets[2]]; + object param4 = input[argOffsets[3]]; + object param5 = input[argOffsets[4]]; + object param6 = input[argOffsets[5]]; + object param7 = input[argOffsets[6]]; + object param8 = input[argOffsets[7]]; + object param9 = input[argOffsets[8]]; + object param10 = input[argOffsets[9]]; + return _func( + (_sameT[0] ??= param1 is T1) ? (T1)param1 : ConvertTo(param1), + (_sameT[1] ??= param2 is T2) ? (T2)param2 : ConvertTo(param2), + (_sameT[2] ??= param3 is T3) ? (T3)param3 : ConvertTo(param3), + (_sameT[3] ??= param4 is T4) ? (T4)param4 : ConvertTo(param4), + (_sameT[4] ??= param5 is T5) ? (T5)param5 : ConvertTo(param5), + (_sameT[5] ??= param6 is T6) ? (T6)param6 : ConvertTo(param6), + (_sameT[6] ??= param7 is T7) ? (T7)param7 : ConvertTo(param7), + (_sameT[7] ??= param8 is T8) ? (T8)param8 : ConvertTo(param8), + (_sameT[8] ??= param9 is T9) ? (T9)param9 : ConvertTo(param9), + (_sameT[9] ??= param10 is T10) ? (T10)param10 : ConvertTo(param10)); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/RelationalGroupedDataset.cs b/src/spark/Flowthru.Spark/Sql/RelationalGroupedDataset.cs new file mode 100644 index 00000000..75c13572 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/RelationalGroupedDataset.cs @@ -0,0 +1,219 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Apache.Arrow; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql.Expressions; +using Flowthru.Spark.Sql.Types; +using Flowthru.Spark.Utils; +using FxDataFrame = Microsoft.Data.Analysis.DataFrame; + +namespace Flowthru.Spark.Sql +{ + /// + /// A set of methods for aggregations on a DataFrame. + /// + public sealed class RelationalGroupedDataset : IJvmObjectReferenceProvider + { + private readonly DataFrame _dataFrame; + + internal RelationalGroupedDataset(JvmObjectReference jvmObject, DataFrame dataFrame) + { + Reference = jvmObject; + _dataFrame = dataFrame; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Compute aggregates by specifying a series of aggregate columns. + /// + /// Column to aggregate on + /// Additional columns to aggregate on + /// New DataFrame object with aggregation applied + public DataFrame Agg(Column expr, params Column[] exprs) => + new DataFrame((JvmObjectReference)Reference.Invoke("agg", expr, exprs)); + + /// + /// Count the number of rows for each group. + /// + /// New DataFrame object with count applied + public DataFrame Count() => + new DataFrame((JvmObjectReference)Reference.Invoke("count")); + + /// + /// Compute the mean value for each numeric columns for each group. + /// + /// Name of columns to compute mean on + /// New DataFrame object with mean applied + public DataFrame Mean(params string[] colNames) => + new DataFrame((JvmObjectReference)Reference.Invoke("mean", (object)colNames)); + + /// + /// Compute the max value for each numeric columns for each group. + /// + /// Name of columns to compute max on + /// New DataFrame object with max applied + public DataFrame Max(params string[] colNames) => + new DataFrame((JvmObjectReference)Reference.Invoke("max", (object)colNames)); + + /// + /// Compute the average value for each numeric columns for each group. + /// + /// Name of columns to compute average on + /// New DataFrame object with average applied + public DataFrame Avg(params string[] colNames) => + new DataFrame((JvmObjectReference)Reference.Invoke("avg", (object)colNames)); + + /// + /// Compute the min value for each numeric columns for each group. + /// + /// Name of columns to compute min on + /// New DataFrame object with min applied + public DataFrame Min(params string[] colNames) => + new DataFrame((JvmObjectReference)Reference.Invoke("min", (object)colNames)); + + /// + /// Compute the sum for each numeric columns for each group. + /// + /// Name of columns to compute sum on + /// New DataFrame object with sum applied + public DataFrame Sum(params string[] colNames) => + new DataFrame((JvmObjectReference)Reference.Invoke("sum", (object)colNames)); + + /// + /// Pivots a column of the current DataFrame and performs the specified aggregation. + /// + /// Name of the column to pivot + /// New RelationalGroupedDataset object with pivot applied + public RelationalGroupedDataset Pivot(string pivotColumn) => + new RelationalGroupedDataset( + (JvmObjectReference)Reference.Invoke("pivot", pivotColumn), _dataFrame); + + /// + /// Pivots a column of the current DataFrame and performs the specified aggregation. + /// + /// Name of the column to pivot of type string + /// List of values that will be translated to columns in the + /// output DataFrame. + /// New RelationalGroupedDataset object with pivot applied + public RelationalGroupedDataset Pivot(string pivotColumn, IEnumerable values) => + new RelationalGroupedDataset( + (JvmObjectReference)Reference.Invoke("pivot", pivotColumn, values), _dataFrame); + + /// + /// Pivots a column of the current DataFrame and performs the specified aggregation. + /// + /// The column to pivot + /// New RelationalGroupedDataset object with pivot applied + public RelationalGroupedDataset Pivot(Column pivotColumn) => + new RelationalGroupedDataset( + (JvmObjectReference)Reference.Invoke("pivot", pivotColumn), _dataFrame); + + /// + /// Pivots a column of the current DataFrame and performs the specified aggregation. + /// + /// The column to pivot of type + /// List of values that will be translated to columns in the + /// output DataFrame. + /// New RelationalGroupedDataset object with pivot applied + public RelationalGroupedDataset Pivot(Column pivotColumn, IEnumerable values) => + new RelationalGroupedDataset( + (JvmObjectReference)Reference.Invoke("pivot", pivotColumn, values), _dataFrame); + + /// + /// Maps each group of the current DataFrame using a UDF and + /// returns the result as a DataFrame. + /// + /// The user-defined function should take an + /// and return another . For each group, all + /// columns are passed together as an to the user-function and + /// the returned FxDataFrame are combined as a DataFrame. + /// + /// The returned can be of arbitrary length and its schema must + /// match . + /// + /// + /// The that represents the schema of the return data set. + /// + /// A grouped map user-defined function. + /// New DataFrame object with the UDF applied. + public DataFrame Apply(StructType returnType, Func func) + { + DataFrameGroupedMapWorkerFunction.ExecuteDelegate wrapper = + new DataFrameGroupedMapUdfWrapper(func).Execute; + + var udf = UserDefinedFunction.Create( + Reference.Jvm, + func.Method.ToString(), + CommandSerDe.Serialize( + wrapper, + CommandSerDe.SerializedMode.Row, + CommandSerDe.SerializedMode.Row), + UdfUtils.PythonEvalType.SQL_GROUPED_MAP_PANDAS_UDF, + returnType.Json); + + IReadOnlyList columnNames = _dataFrame.Columns(); + var columns = new Column[columnNames.Count]; + for (int i = 0; i < columnNames.Count; ++i) + { + columns[i] = _dataFrame[columnNames[i]]; + } + + Column udfColumn = udf.Apply(columns); + + return new DataFrame((JvmObjectReference)Reference.Invoke( + "flatMapGroupsInPandas", + udfColumn.Expr())); + } + + /// + /// Maps each group of the current DataFrame using a UDF and + /// returns the result as a DataFrame. + /// + /// The user-defined function should take an Apache Arrow RecordBatch + /// and return another Apache Arrow RecordBatch. For each group, all + /// columns are passed together as a RecordBatch to the user-function and + /// the returned RecordBatch are combined as a DataFrame. + /// + /// The returned can be of arbitrary length and its + /// schema must match . + /// + /// + /// The that represents the shape of the return data set. + /// + /// A grouped map user-defined function. + /// New DataFrame object with the UDF applied. + public DataFrame Apply(StructType returnType, Func func) + { + ArrowGroupedMapWorkerFunction.ExecuteDelegate wrapper = + new ArrowGroupedMapUdfWrapper(func).Execute; + + UserDefinedFunction udf = UserDefinedFunction.Create( + Reference.Jvm, + func.Method.ToString(), + CommandSerDe.Serialize( + wrapper, + CommandSerDe.SerializedMode.Row, + CommandSerDe.SerializedMode.Row), + UdfUtils.PythonEvalType.SQL_GROUPED_MAP_PANDAS_UDF, + returnType.Json); + + IReadOnlyList columnNames = _dataFrame.Columns(); + var columns = new Column[columnNames.Count]; + for (int i = 0; i < columnNames.Count; ++i) + { + columns[i] = _dataFrame[columnNames[i]]; + } + + Column udfColumn = udf.Apply(columns); + + return new DataFrame((JvmObjectReference)Reference.Invoke( + "flatMapGroupsInPandas", + udfColumn.Expr())); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Row.cs b/src/spark/Flowthru.Spark/Sql/Row.cs new file mode 100644 index 00000000..69d6c01e --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Row.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Flowthru.Spark.Sql.Types; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Sql +{ + /// + /// Represents a row object in RDD, equivalent to GenericRowWithSchema in Spark. + /// + public sealed class Row + { + private readonly GenericRow _genericRow; + + /// + /// Constructor for the Row class. + /// + /// Column values for a row + /// Schema associated with a row + internal Row(object[] values, StructType schema) + { + _genericRow = new GenericRow(values); + Schema = schema; + + int schemaColumnCount = Schema.Fields.Count; + if (Size() != schemaColumnCount) + { + throw new Exception( + $"Column count mismatches: data:{Size()}, schema:{schemaColumnCount}"); + } + + Convert(); + } + + /// + /// Constructor for the schema-less Row class used for chained UDFs. + /// + /// GenericRow object + internal Row(GenericRow genericRow) + { + _genericRow = genericRow; + } + + /// + /// Returns schema-less Row which can happen within chained UDFs (same behavior as PySpark). + /// + /// + /// The use of this conversion operator is discouraged except for the UDF that returns + /// a Row object. + /// + /// schema-less Row + public static implicit operator Row(GenericRow genericRow) + { + return new Row(genericRow); + } + + /// + /// Schema associated with this row. + /// + public StructType Schema { get; } + + /// + /// Values representing this row. + /// + public object[] Values => _genericRow.Values; + + /// + /// Returns the number of columns in this row. + /// + /// Number of columns in this row + public int Size() => _genericRow.Size(); + + /// + /// Returns the column value at the given index. + /// + /// Index to look up + /// A column value + public object this[int index] => Get(index); + + /// + /// Returns the column value at the given index. + /// + /// Index to look up + /// A column value + public object Get(int index) => _genericRow.Get(index); + /// + /// Returns the column value whose column name is given. + /// + /// Column name to look up + /// A column value + public object Get(string columnName) => + Get(Schema.Fields.FindIndex(f => f.Name == columnName)); + + /// + /// Returns the string version of this row. + /// + /// String version of this row + public override string ToString() => _genericRow.ToString(); + + /// + /// Returns the column value at the given index, as a type T. + /// TODO: If the original type is "long" and its value can be + /// fit into the "int", Pickler will serialize the value as int. + /// Since the value is boxed, will throw an exception. + /// + /// Type to convert to + /// Index to look up + /// A column value as a type T + public T GetAs(int index) => TypeConverter.ConvertTo(Get(index)); + + /// + /// Returns the column value whose column name is given, as a type T. + /// TODO: If the original type is "long" and its value can be + /// fit into the "int", Pickler will serialize the value as int. + /// Since the value is boxed, will throw an exception. + /// + /// Type to convert to + /// Column name to look up + /// A column value as a type T + public T GetAs(string columnName) => TypeConverter.ConvertTo(Get(columnName)); + + /// + /// Checks if the given object is same as the current object. + /// + /// Other object to compare against + /// True if the other object is equal. + public override bool Equals(object obj) => + ReferenceEquals(this, obj) || + ((obj is Row row) && _genericRow.Equals(row._genericRow)) && Schema.Equals(row.Schema); + + /// + /// Returns the hash code of the current object. + /// + /// The hash code of the current object + public override int GetHashCode() => base.GetHashCode(); + + /// + /// Converts the values to .NET values. Currently, only the simple types such as + /// int, string, etc. are supported (which are already converted correctly by + /// the Pickler). Note that explicit type checks against the schema are not performed. + /// + private void Convert() + { + for (int i = 0; i < Size(); ++i) + { + DataType dataType = Schema.Fields[i].DataType; + if (dataType.NeedConversion()) + { + Values[i] = dataType.FromInternal(Values[i]); + } + } + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/RowCollector.cs b/src/spark/Flowthru.Spark/Sql/RowCollector.cs new file mode 100644 index 00000000..1925ecfe --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/RowCollector.cs @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Network; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Sql +{ + /// + /// RowCollector collects Row objects from a socket. + /// + internal sealed class RowCollector + { + /// + /// Collects pickled row objects from the given socket. + /// + /// Socket the get the stream from. + /// Collection of row objects. + public IEnumerable Collect(ISocketWrapper socket) + { + Stream inputStream = socket.InputStream; + + int? length; + while (((length = SerDe.ReadBytesLength(inputStream)) != null) && + (length.GetValueOrDefault() > 0)) + { + object[] unpickledObjects = + PythonSerDe.GetUnpickledObjects(inputStream, length.GetValueOrDefault()); + + foreach (object unpickled in unpickledObjects) + { + yield return unpickled as Row; + } + } + } + + /// + /// Collects pickled row objects from the given socket. Collects rows in partitions + /// by leveraging . + /// + /// Socket the get the stream from. + /// The JVM socket auth server. + /// Collection of row objects. + public IEnumerable Collect(ISocketWrapper socket, JvmObjectReference server) => + new LocalIteratorFromSocket(socket, server); + + /// + /// LocalIteratorFromSocket creates a synchronous local iterable over + /// a socket. + /// + /// Note that the implementation mirrors _local_iterator_from_socket in + /// PySpark: spark/python/pyspark/rdd.py + /// + private class LocalIteratorFromSocket : IEnumerable + { + private readonly ISocketWrapper _socket; + private readonly JvmObjectReference _server; + + private int _readStatus = 1; + private IEnumerable _currentPartitionRows = null; + + internal LocalIteratorFromSocket(ISocketWrapper socket, JvmObjectReference server) + { + _socket = socket; + _server = server; + } + + ~LocalIteratorFromSocket() + { + // If iterator is not fully consumed. + if ((_readStatus == 1) && (_currentPartitionRows != null)) + { + try + { + // Finish consuming partition data stream. + foreach (Row _ in _currentPartitionRows) + { + } + + // Tell Java to stop sending data and close connection. + Stream outputStream = _socket.OutputStream; + SerDe.Write(outputStream, 0); + outputStream.Flush(); + } + catch + { + // Ignore any errors, socket may be automatically closed + // when garbage-collected. + } + } + } + + public IEnumerator GetEnumerator() + { + Stream inputStream = _socket.InputStream; + Stream outputStream = _socket.OutputStream; + + while (_readStatus == 1) + { + // Request next partition data from Java. + SerDe.Write(outputStream, 1); + outputStream.Flush(); + + // If response is 1 then there is a partition to read, if 0 then + // fully consumed. + _readStatus = SerDe.ReadInt32(inputStream); + if (_readStatus == 1) + { + // Load the partition data from stream and read each item. + _currentPartitionRows = new RowCollector().Collect(_socket); + foreach (Row row in _currentPartitionRows) + { + yield return row; + } + } + else if (_readStatus == -1) + { + // An error occurred, join serving thread and raise any exceptions from + // the JVM. The exception stack trace will appear in the driver logs. + _server.Invoke("getResult"); + } + else + { + Debug.Assert(_readStatus == 0); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/RowConstructor.cs b/src/spark/Flowthru.Spark/Sql/RowConstructor.cs new file mode 100644 index 00000000..7b8f1f16 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/RowConstructor.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Flowthru.Spark.Sql.Types; +using Razorvine.Pickle; + +namespace Flowthru.Spark.Sql +{ + /// + /// RowConstructor is a custom unpickler for GenericRowWithSchema in Spark. + /// Refer to spark/sql/core/src/main/scala/org/apache/spark/sql/execution/python/ + /// EvaluatePython.scala how GenericRowWithSchema is being pickled. + /// + internal sealed class RowConstructor : IObjectConstructor + { + /// + /// Cache the schemas of the rows being received. Multiple schemas may be + /// sent per batch if there are nested rows contained in the row. Note that + /// this is thread local variable because one RowConstructor object is + /// registered to the Unpickler and there could be multiple threads unpickling + /// the data using the same registered object. + /// + [ThreadStatic] + private static IDictionary s_schemaCache; + + /// + /// Used by Unpickler to pass unpickled schema for handling. The Unpickler + /// will reuse the object when + /// it needs to start constructing a . The schema is passed + /// to and the returned + /// is used to build the rest of the . + /// + /// Unpickled schema + /// + /// New object capturing the schema. + /// + public object construct(object[] args) + { + if (s_schemaCache is null) + { + s_schemaCache = new Dictionary(); + } + + Debug.Assert((args != null) && (args.Length == 1) && (args[0] is string)); + return new RowWithSchemaConstructor(GetSchema(s_schemaCache, (string)args[0])); + } + + /// + /// Clears the schema cache. Spark sends rows in batches and for each + /// row there is an accompanying set of schemas and row entries. If the + /// schema was not cached, then it would need to be parsed and converted + /// to a StructType for every row in the batch. A new batch may contain + /// rows from a different table, so calling after each + /// batch would aid in preventing the cache from growing too large. + /// Caching the schemas for each batch, ensures that each schema is + /// only parsed and converted to a StructType once per batch. + /// + internal void Reset() + { + s_schemaCache?.Clear(); + } + + private static StructType GetSchema(IDictionary schemaCache, string schemaString) + { + if (!schemaCache.TryGetValue(schemaString, out StructType schema)) + { + schema = (StructType)DataType.ParseDataType(schemaString); + schemaCache.Add(schemaString, schema); + } + + return schema; + } + } + + /// + /// Created from and subsequently used + /// by the Unpickler to construct a . + /// + internal sealed class RowWithSchemaConstructor : IObjectConstructor + { + private readonly StructType _schema; + + internal RowWithSchemaConstructor(StructType schema) + { + _schema = schema; + } + + /// + /// Used by Unpickler to pass unpickled row values for handling. + /// + /// Unpickled row values. + /// Row object. + public object construct(object[] args) => new Row(args, _schema); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/RuntimeConfig.cs b/src/spark/Flowthru.Spark/Sql/RuntimeConfig.cs new file mode 100644 index 00000000..21943d15 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/RuntimeConfig.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql +{ + /// + /// Runtime configuration interface for Spark. + /// + public sealed class RuntimeConfig : IJvmObjectReferenceProvider + { + internal RuntimeConfig(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + + /// + /// Sets the given Spark runtime configuration property. + /// + /// Config name + /// Config value + public void Set(string key, string value) => Reference.Invoke("set", key, value); + + /// + /// Sets the given Spark runtime configuration property. + /// + /// Config name + /// Config value + public void Set(string key, bool value) => Reference.Invoke("set", key, value); + + /// + /// Sets the given Spark runtime configuration property. + /// + /// Config name + /// Config value + public void Set(string key, long value) => Reference.Invoke("set", key, value); + + /// + /// Returns the value of Spark runtime configuration property for the given key. + /// + /// Key to use + public string Get(string key) => (string)Reference.Invoke("get", key); + + /// + /// Returns the value of Spark runtime configuration property for the given key. + /// + /// Key to use + /// Default value to use + public string Get(string key, string defaultValue) => + (string)Reference.Invoke("get", key, defaultValue); + + /// + /// Resets the configuration property for the given key. + /// + /// Key to unset + public void Unset(string key) => Reference.Invoke("unset", key); + + /// + /// Indicates whether the configuration property with the given key + /// is modifiable in the current session. + /// + /// Key to check + /// + /// true if the configuration property is modifiable. For static SQL, Spark + /// Core, invalid(not existing) and other non-modifiable configuration properties, + /// the returned value is false. + /// + public bool IsModifiable(string key) => (bool)Reference.Invoke("isModifiable", key); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/SaveMode.cs b/src/spark/Flowthru.Spark/Sql/SaveMode.cs new file mode 100644 index 00000000..6e2c35e1 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/SaveMode.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Flowthru.Spark.Sql +{ + /// + /// SaveMode is used to specify the expected behavior of saving a DataFrame to a data source. + /// + public enum SaveMode + { + /// + /// Append mode means that when saving a DataFrame to a data source, if data/table already + /// exists, contents of the DataFrame are expected to be appended to existing data. + /// + Append, + + /// + /// Overwrite mode means that when saving a DataFrame to a data source, + /// if data/table already exists, existing data is expected to be overwritten by the + /// contents of the DataFrame. + /// + Overwrite, + + /// + /// ErrorIfExists mode means that when saving a DataFrame to a data source, if data already + /// exists, an exception is expected to be thrown. + /// + ErrorIfExists, + + /// + /// Ignore mode means that when saving a DataFrame to a data source, if data already exists, + /// the save operation is expected to not save the contents of the DataFrame and to not + /// change the existing data. + /// + Ignore + } +} diff --git a/src/spark/Flowthru.Spark/Sql/SparkSession.cs b/src/spark/Flowthru.Spark/Sql/SparkSession.cs new file mode 100644 index 00000000..c0386c61 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/SparkSession.cs @@ -0,0 +1,417 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Internal.Scala; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql.Streaming; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.Sql +{ + /// + /// The entry point to programming Spark with the Dataset and DataFrame API. + /// + public sealed class SparkSession : IDisposable, IJvmObjectReferenceProvider + { + private readonly Lazy _sparkContext; + private readonly Lazy _catalog; + + private static readonly string s_sparkSessionClassName = + "org.apache.spark.sql.SparkSession"; + + /// + /// Constructor for SparkSession. + /// + /// Reference to the JVM SparkSession object + internal SparkSession(JvmObjectReference jvmObject) + { + Reference = jvmObject; + _sparkContext = new Lazy( + () => new SparkContext( + (JvmObjectReference)Reference.Invoke("sparkContext"))); + _catalog = new Lazy( + () => new Catalog.Catalog((JvmObjectReference)Reference.Invoke("catalog"))); + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Returns SparkContext object associated with this SparkSession. + /// + public SparkContext SparkContext => _sparkContext.Value; + + /// + /// Interface through which the user may create, drop, alter or query underlying databases, + /// tables, functions etc. + /// + /// Catalog object + public Catalog.Catalog Catalog => _catalog.Value; + + /// + /// Creates a Builder object for SparkSession. + /// + /// Builder object + public static Builder Builder() => new Builder(); + + /// + /// Changes the SparkSession that will be returned in this thread when + /// is called. This can be used to ensure that a given + /// thread receives a SparkSession with an isolated session, instead of the global + /// (first created) context. + /// + /// SparkSession object + public static void SetActiveSession(SparkSession session) => + session.Reference.Jvm.CallStaticJavaMethod( + s_sparkSessionClassName, "setActiveSession", session); + + /// + /// Clears the active SparkSession for current thread. Subsequent calls to + /// will return the first created context + /// instead of a thread-local override. + /// + public static void ClearActiveSession() => + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_sparkSessionClassName, "clearActiveSession"); + + /// + /// Returns the active SparkSession for the current thread, returned by the builder. + /// + /// Return null, when calling this function on executors + public static SparkSession GetActiveSession() + { + var optionalSession = new Option( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_sparkSessionClassName, "getActiveSession")); + + return optionalSession.IsDefined() + ? new SparkSession((JvmObjectReference)optionalSession.Get()) + : null; + } + + /// + /// Sets the default SparkSession that is returned by the builder. + /// + /// SparkSession object + public static void SetDefaultSession(SparkSession session) => + session.Reference.Jvm.CallStaticJavaMethod( + s_sparkSessionClassName, "setDefaultSession", session); + + /// + /// Clears the default SparkSession that is returned by the builder. + /// + public static void ClearDefaultSession() => + SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_sparkSessionClassName, "clearDefaultSession"); + + /// + /// Returns the default SparkSession that is returned by the builder. + /// + /// SparkSession object or null if called on executors + public static SparkSession GetDefaultSession() + { + var optionalSession = new Option( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_sparkSessionClassName, "getDefaultSession")); + + return optionalSession.IsDefined() + ? new SparkSession((JvmObjectReference)optionalSession.Get()) + : null; + } + + /// + /// Returns the currently active SparkSession, otherwise the default one. + /// If there is no default SparkSession, throws an exception. + /// + /// SparkSession object + [Since(Versions.V2_4_0)] + public static SparkSession Active() => + new SparkSession( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_sparkSessionClassName, "active")); + + /// + /// Synonym for Stop(). + /// + public void Dispose() + { + Stop(); + GC.SuppressFinalize(this); + } + + /// + /// Runtime configuration interface for Spark. + /// + /// This is the interface through which the user can get and set all Spark and Hadoop + /// configurations that are relevant to Spark SQL. When getting the value of a config, + /// this defaults to the value set in the underlying SparkContext, if any. + /// + /// + /// The RuntimeConfig object + public RuntimeConfig Conf() => + new RuntimeConfig((JvmObjectReference)Reference.Invoke("conf")); + + /// + /// Returns a that allows managing all the + /// instances active on this context. + /// + /// object + public StreamingQueryManager Streams() => + new StreamingQueryManager((JvmObjectReference)Reference.Invoke("streams")); + + /// + /// Start a new session with isolated SQL configurations, temporary tables, registered + /// functions are isolated, but sharing the underlying SparkContext and cached data. + /// + /// + /// Other than the SparkContext, all shared state is initialized lazily. + /// This method will force the initialization of the shared state to ensure that parent + /// and child sessions are set up with the same shared state. If the underlying catalog + /// implementation is Hive, this will initialize the metastore, which may take some time. + /// + /// New SparkSession object + public SparkSession NewSession() => + new SparkSession((JvmObjectReference)Reference.Invoke("newSession")); + + /// + /// Returns a string that represents the version of Spark on which this application is running. + /// + /// + /// A string that represents the version of Spark on which this application is running. + /// + public string Version() => (string)Reference.Invoke("version"); + + /// + /// Returns the specified table/view as a DataFrame. + /// + /// Name of a table or view + /// DataFrame object + public DataFrame Table(string tableName) => + new DataFrame((JvmObjectReference)Reference.Invoke("table", tableName)); + + /// + /// Creates a from an containing + /// s using the given schema. + /// It is important to make sure that the structure of every of + /// the provided matches + /// the provided schema. Otherwise, there will be runtime exception. + /// + /// List of Row objects + /// Schema as StructType + /// DataFrame object + public DataFrame CreateDataFrame(IEnumerable data, StructType schema) => + new DataFrame((JvmObjectReference)Reference.Invoke( + "createDataFrame", + data, + DataType.FromJson(Reference.Jvm, schema.Json))); + + /// + /// Creates a Dataframe given data as of type + /// + /// of type + /// Dataframe object + public DataFrame CreateDataFrame(IEnumerable data) => + CreateDataFrame(ToGenericRows(data), SchemaWithSingleColumn(new IntegerType(), false)); + + /// + /// Creates a Dataframe given data as of type + /// + /// + /// of type + /// + /// Dataframe object + public DataFrame CreateDataFrame(IEnumerable data) => + CreateDataFrame(ToGenericRows(data), SchemaWithSingleColumn(new IntegerType())); + + /// + /// Creates a Dataframe given data as of type + /// + /// + /// of type + /// Dataframe object + public DataFrame CreateDataFrame(IEnumerable data) => + CreateDataFrame(ToGenericRows(data), SchemaWithSingleColumn(new StringType())); + + /// + /// Creates a Dataframe given data as of type + /// + /// + /// of type + /// Dataframe object + public DataFrame CreateDataFrame(IEnumerable data) => + CreateDataFrame(ToGenericRows(data), SchemaWithSingleColumn(new DoubleType(), false)); + + /// + /// Creates a Dataframe given data as of type + /// + /// + /// of type + /// + /// Dataframe object + public DataFrame CreateDataFrame(IEnumerable data) => + CreateDataFrame(ToGenericRows(data), SchemaWithSingleColumn(new DoubleType())); + + /// + /// Creates a Dataframe given data as of type + /// + /// of type + /// Dataframe object + public DataFrame CreateDataFrame(IEnumerable data) => + CreateDataFrame(ToGenericRows(data), SchemaWithSingleColumn(new BooleanType(), false)); + + /// + /// Creates a Dataframe given data as of type + /// + /// + /// of type + /// + /// Dataframe object + public DataFrame CreateDataFrame(IEnumerable data) => + CreateDataFrame(ToGenericRows(data), SchemaWithSingleColumn(new BooleanType())); + + /// + /// Creates a Dataframe given data as of type + /// + /// of type + /// Dataframe object + public DataFrame CreateDataFrame(IEnumerable data) => + CreateDataFrame(ToGenericRows(data), SchemaWithSingleColumn(new DateType())); + + /// + /// Creates a Dataframe given data as of type + /// + /// + /// of type + /// Dataframe object + public DataFrame CreateDataFrame(IEnumerable data) => + CreateDataFrame(ToGenericRows(data), SchemaWithSingleColumn(new TimestampType())); + + /// + /// Executes a SQL query using Spark, returning the result as a DataFrame. + /// + /// SQL query text + /// DataFrame object + public DataFrame Sql(string sqlText) => + new DataFrame((JvmObjectReference)Reference.Invoke("sql", sqlText)); + + /// + /// Execute an arbitrary string command inside an external execution engine rather than + /// Spark. This could be useful when user wants to execute some commands out of Spark. For + /// example, executing custom DDL/DML command for JDBC, creating index for ElasticSearch, + /// creating cores for Solr and so on. + /// The command will be eagerly executed after this method is called and the returned + /// DataFrame will contain the output of the command(if any). + /// + /// The class name of the runner that implements + /// `ExternalCommandRunner` + /// The target command to be executed + /// The options for the runner + /// >DataFrame object + [Since(Versions.V3_0_0)] + public DataFrame ExecuteCommand( + string runner, + string command, + Dictionary options) => + new DataFrame((JvmObjectReference)Reference.Invoke( + "executeCommand", + runner, + command, + options)); + + /// + /// Returns a DataFrameReader that can be used to read non-streaming data in + /// as a DataFrame. + /// + /// DataFrameReader object + public DataFrameReader Read() => + new DataFrameReader((JvmObjectReference)Reference.Invoke("read")); + + /// + /// Creates a DataFrame with a single column named id, containing elements in + /// a range from 0 to end (exclusive) with step value 1. + /// + /// The end value (exclusive) + /// DataFrame object + public DataFrame Range(long end) => + new DataFrame((JvmObjectReference)Reference.Invoke("range", end)); + + /// + /// Creates a DataFrame with a single column named id, containing elements in + /// a range from start to end (exclusive) with step value 1. + /// + /// The start value + /// The end value (exclusive) + /// DataFrame object + public DataFrame Range(long start, long end) => + new DataFrame((JvmObjectReference)Reference.Invoke("range", start, end)); + + /// + /// Creates a DataFrame with a single column named id, containing elements in + /// a range from start to end (exclusive) with a step value. + /// + /// The start value + /// The end value (exclusive) + /// Step value to use when creating the range + /// DataFrame object + public DataFrame Range(long start, long end, long step) => + new DataFrame((JvmObjectReference)Reference.Invoke("range", start, end, step)); + + /// + /// Creates a DataFrame with a single column named id, containing elements in + /// a range from start to end (exclusive) with a step value, with partition + /// number specified. + /// + /// The start value + /// The end value (exclusive) + /// Step value to use when creating the range + /// The number of partitions of the DataFrame + /// DataFrame object + public DataFrame Range(long start, long end, long step, int numPartitions) => + new DataFrame( + (JvmObjectReference)Reference.Invoke("range", start, end, step, numPartitions)); + + /// + /// Returns a DataStreamReader that can be used to read streaming data in as a DataFrame. + /// + /// DataStreamReader object + public DataStreamReader ReadStream() => + new DataStreamReader((JvmObjectReference)Reference.Invoke("readStream")); + + /// + /// Returns UDFRegistraion object with which user-defined functions (UDF) can + /// be registered. + /// + /// UDFRegistration object + public UdfRegistration Udf() => + new UdfRegistration((JvmObjectReference)Reference.Invoke("udf")); + + /// + /// Stops the underlying SparkContext. + /// + public void Stop() => Reference.Invoke("stop"); + + /// + /// Returns a single column schema of the given datatype. + /// + /// Datatype of the column + /// Indicates if values of the column can be null + /// Schema as StructType + private StructType SchemaWithSingleColumn(DataType dataType, bool isNullable = true) => + new StructType(new[] { new StructField("_1", dataType, isNullable) }); + + /// + /// This method is transforming each element of IEnumerable of type T input into a single + /// columned GenericRow. + /// + /// Datatype of values in rows + /// List of values of type T + /// of type + private IEnumerable ToGenericRows(IEnumerable rows) => + rows.Select(r => new GenericRow(new object[] { r })); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/StorageLevel.cs b/src/spark/Flowthru.Spark/Sql/StorageLevel.cs new file mode 100644 index 00000000..7e0ef357 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/StorageLevel.cs @@ -0,0 +1,238 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql +{ + /// + /// Flags for controlling the storage of an RDD. Each StorageLevel records whether to use + /// memory, whether to drop the RDD to disk if it falls out of memory, whether to keep the + /// data in memory in a JAVA-specific serialized format, and whether to replicate the RDD + /// partitions on multiple nodes. Also contains static properties for some commonly used + /// storage levels, MEMORY_ONLY. + /// + public sealed class StorageLevel : IJvmObjectReferenceProvider + { + private static readonly string s_storageLevelClassName = + "org.apache.spark.storage.StorageLevel"; + private static StorageLevel s_none; + private static StorageLevel s_diskOnly; + private static StorageLevel s_diskOnly2; + private static StorageLevel s_memoryOnly; + private static StorageLevel s_memoryOnly2; + private static StorageLevel s_memoryOnlySer; + private static StorageLevel s_memoryOnlySer2; + private static StorageLevel s_memoryAndDisk; + private static StorageLevel s_memoryAndDisk2; + private static StorageLevel s_memoryAndDiskSer; + private static StorageLevel s_memoryAndDiskSer2; + private static StorageLevel s_offHeap; + private bool? _useDisk; + private bool? _useMemory; + private bool? _useOffHeap; + private bool? _deserialized; + private int? _replication; + + internal StorageLevel(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public StorageLevel( + bool useDisk, + bool useMemory, + bool useOffHeap, + bool deserialized, + int replication = 1) + : this( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "apply", + useDisk, + useMemory, + useOffHeap, + deserialized, + replication)) + { + _useDisk = useDisk; + _useMemory = useMemory; + _useOffHeap = useOffHeap; + _deserialized = deserialized; + _replication = replication; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Returns the StorageLevel object with all parameters set to false. + /// + public static StorageLevel NONE => + s_none ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "NONE")); + + /// + /// Returns the StorageLevel to Disk, serialized and replicated once. + /// + public static StorageLevel DISK_ONLY => + s_diskOnly ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "DISK_ONLY")); + + /// + /// Returns the StorageLevel to Disk, serialized and replicated twice. + /// + public static StorageLevel DISK_ONLY_2 => + s_diskOnly2 ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "DISK_ONLY_2")); + + /// + /// Returns the StorageLevel to Memory, deserialized and replicated once. + /// + public static StorageLevel MEMORY_ONLY => + s_memoryOnly ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "MEMORY_ONLY")); + + /// + /// Returns the StorageLevel to Memory, deserialized and replicated twice. + /// + public static StorageLevel MEMORY_ONLY_2 => + s_memoryOnly2 ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "MEMORY_ONLY_2")); + + /// + /// Returns the StorageLevel to Memory, serialized and replicated once. + /// + public static StorageLevel MEMORY_ONLY_SER => + s_memoryOnlySer ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "MEMORY_ONLY_SER")); + + /// + /// Returns the StorageLevel to Memory, serialized and replicated twice. + /// + public static StorageLevel MEMORY_ONLY_SER_2 => + s_memoryOnlySer2 ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "MEMORY_ONLY_SER_2")); + + /// + /// Returns the StorageLevel to Disk and Memory, deserialized and replicated once. + /// + public static StorageLevel MEMORY_AND_DISK => + s_memoryAndDisk ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "MEMORY_AND_DISK")); + + /// + /// Returns the StorageLevel to Disk and Memory, deserialized and replicated twice. + /// + public static StorageLevel MEMORY_AND_DISK_2 => + s_memoryAndDisk2 ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "MEMORY_AND_DISK_2")); + + /// + /// Returns the StorageLevel to Disk and Memory, serialized and replicated once. + /// + public static StorageLevel MEMORY_AND_DISK_SER => + s_memoryAndDiskSer ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "MEMORY_AND_DISK_SER")); + + /// + /// Returns the StorageLevel to Disk and Memory, serialized and replicated twice. + /// + public static StorageLevel MEMORY_AND_DISK_SER_2 => + s_memoryAndDiskSer2 ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "MEMORY_AND_DISK_SER_2")); + + /// + /// Returns the StorageLevel to Disk, Memory and Offheap, serialized and replicated once. + /// + public static StorageLevel OFF_HEAP => + s_offHeap ??= new StorageLevel( + (JvmObjectReference)SparkEnvironment.JvmBridge.CallStaticJavaMethod( + s_storageLevelClassName, + "OFF_HEAP")); + + /// + /// Returns bool value of UseDisk of this StorageLevel. + /// + public bool UseDisk => _useDisk ??= (bool)Reference.Invoke("useDisk"); + + /// + /// Returns bool value of UseMemory of this StorageLevel. + /// + public bool UseMemory => _useMemory ??= (bool)Reference.Invoke("useMemory"); + + /// + /// Returns bool value of UseOffHeap of this StorageLevel. + /// + public bool UseOffHeap => _useOffHeap ??= (bool)Reference.Invoke("useOffHeap"); + + /// + /// Returns bool value of Deserialized of this StorageLevel. + /// + public bool Deserialized => _deserialized ??= (bool)Reference.Invoke("deserialized"); + + /// + /// Returns int value of Replication of this StorageLevel. + /// + public int Replication => _replication ??= (int)Reference.Invoke("replication"); + + /// + /// Returns the description string of this StorageLevel. + /// + /// Description as string. + public string Description() => (string)Reference.Invoke("description"); + + /// + /// Returns the string representation of this StorageLevel. + /// + /// representation as string value. + public override string ToString() => (string)Reference.Invoke("toString"); + + /// + /// Checks if the given object is same as the current object. + /// + /// Other object to compare against + /// True if the other object is equal. + public override bool Equals(object obj) + { + if (!(obj is StorageLevel that)) + { + return false; + } + + return (UseDisk == that.UseDisk) && (UseMemory == that.UseMemory) && + (UseOffHeap == that.UseOffHeap) && (Deserialized == that.Deserialized) && + (Replication == that.Replication); + } + + /// + /// Returns the hash code of the current object. + /// + /// The hash code of the current object + public override int GetHashCode() => base.GetHashCode(); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Streaming/DataStreamReader.cs b/src/spark/Flowthru.Spark/Sql/Streaming/DataStreamReader.cs new file mode 100644 index 00000000..f5167537 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Streaming/DataStreamReader.cs @@ -0,0 +1,207 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.Sql.Streaming +{ + /// + /// DataStreamReader provides functionality to load a streaming + /// from external storage systems (e.g. file systems, key-value stores, etc). + /// + public sealed class DataStreamReader : IJvmObjectReferenceProvider + { + internal DataStreamReader(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + + /// + /// Specifies the input data source format. + /// + /// Name of the data source + /// This DataStreamReader object + public DataStreamReader Format(string source) + { + Reference.Invoke("format", source); + return this; + } + + /// + /// Specifies the schema by using . + /// + /// + /// Some data sources (e.g. JSON) can infer the input schema automatically + /// from data. By specifying the schema here, the underlying data source can + /// skip the schema inference step, and thus speed up data loading. + /// + /// The input schema + /// This DataStreamReader object + public DataStreamReader Schema(StructType schema) + { + Reference.Invoke("schema", DataType.FromJson(Reference.Jvm, schema.Json)); + return this; + } + + /// + /// Specifies the schema by using the given DDL-formatted string. + /// + /// + /// Some data sources (e.g. JSON) can infer the input schema automatically + /// from data. By specifying the schema here, the underlying data source can + /// skip the schema inference step, and thus speed up data loading. + /// + /// DDL-formatted string + /// This DataStreamReader object + public DataStreamReader Schema(string schemaString) + { + Reference.Invoke("schema", schemaString); + return this; + } + + /// + /// Adds an input option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataStreamReader object + public DataStreamReader Option(string key, string value) + { + OptionInternal(key, value); + return this; + } + + /// + /// Adds an input option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataStreamReader object + public DataStreamReader Option(string key, bool value) + { + OptionInternal(key, value); + return this; + } + + /// + /// Adds an input option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataStreamReader object + public DataStreamReader Option(string key, long value) + { + OptionInternal(key, value); + return this; + } + + /// + /// Adds an input option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataStreamReader object + public DataStreamReader Option(string key, double value) + { + OptionInternal(key, value); + return this; + } + + /// + /// Adds input options for the underlying data source. + /// + /// Key/value options + /// This DataStreamReader object + public DataStreamReader Options(Dictionary options) + { + Reference.Invoke("options", options); + return this; + } + + /// + /// Loads input data stream in as a , for data streams + /// that don't require a path (e.g.external key-value stores). + /// + /// DataFrame object + public DataFrame Load() => new DataFrame((JvmObjectReference)Reference.Invoke("load")); + + /// + /// Loads input in as a , for data streams that read + /// from some path. + /// + /// File path for streaming + /// DataFrame object + public DataFrame Load(string path) => + new DataFrame((JvmObjectReference)Reference.Invoke("load", path)); + + /// + /// Loads a JSON file stream and returns the results as a . + /// + /// File path for streaming + /// DataFrame object + public DataFrame Json(string path) => LoadSource("json", path); + + /// + /// Loads a CSV file stream and returns the result as a . + /// + /// File path for streaming + /// DataFrame object + public DataFrame Csv(string path) => LoadSource("csv", path); + + /// + /// Loads a ORC file stream and returns the result as a . + /// + /// File path for streaming + /// DataFrame object + public DataFrame Orc(string path) => LoadSource("orc", path); + + /// + /// Loads a Parquet file stream and returns the result as a . + /// + /// File path for streaming + /// DataFrame object + public DataFrame Parquet(string path) => LoadSource("parquet", path); + + /// + /// Define a Streaming DataFrame on a Table. The DataSource corresponding to the table should + /// support streaming mode. + /// + /// Name of the table + /// DataFrame object + [Since(Versions.V3_1_0)] + public DataFrame Table(string tableName) => + new DataFrame((JvmObjectReference)Reference.Invoke("table", tableName)); + + /// + /// Loads text files and returns a whose schema starts + /// with a string column named "value", and followed by partitioned columns + /// if there are any. + /// + /// File path for streaming + /// DataFrame object + public DataFrame Text(string path) => LoadSource("text", path); + + /// + /// Helper function to add given key/value pair as a new option. + /// + /// Name of the option + /// Value of the option + /// This DataFrameReader object + private DataStreamReader OptionInternal(string key, object value) + { + Reference.Invoke("option", key, value); + return this; + } + + /// + /// Helper function to load the source for a given path. + /// + /// Name of the source + /// File path for streaming + /// DataFrame object + private DataFrame LoadSource(string source, string path) => + new DataFrame((JvmObjectReference)Reference.Invoke(source, path)); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Streaming/DataStreamWriter.cs b/src/spark/Flowthru.Spark/Sql/Streaming/DataStreamWriter.cs new file mode 100644 index 00000000..6b77ef63 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Streaming/DataStreamWriter.cs @@ -0,0 +1,272 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql.Types; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Sql.Streaming +{ + /// + /// DataStreamWriter provides functionality to write a streaming + /// to external storage systems (e.g. file systems, key-value stores, etc). + /// + public sealed class DataStreamWriter : IJvmObjectReferenceProvider + { + private readonly DataFrame _df; + + internal DataStreamWriter(JvmObjectReference jvmObject, DataFrame df) + { + Reference = jvmObject; + _df = df; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Specifies how data of a streaming DataFrame is written to a streaming sink. + /// + /// + /// The following mode is supported: + /// "append": Only the new rows in the streaming DataFrame/Dataset will be written to + /// the sink. + /// "complete": All the rows in the streaming DataFrame/Dataset will be written to the sink + /// every time there are some updates. + /// "update": Only the rows that were updated in the streaming DataFrame will + /// be written to the sink every time there are some updates. If the query + /// doesn't contain aggregations, it will be equivalent to `append` mode. + /// + /// Output mode name + /// This DataStreamWriter object + public DataStreamWriter OutputMode(string outputMode) + { + Reference.Invoke("outputMode", outputMode); + return this; + } + + /// + /// Specifies how data of a streaming DataFrame is written to a streaming sink. + /// + /// Output mode enum + /// This DataStreamWriter object + public DataStreamWriter OutputMode(OutputMode outputMode) => + OutputMode(outputMode.ToString()); + + /// + /// Specifies the underlying output data source. + /// + /// Name of the data source + /// This DataStreamWriter object + public DataStreamWriter Format(string source) + { + Reference.Invoke("format", source); + return this; + } + + /// + /// Partitions the output by the given columns on the file system. If specified, + /// the output is laid out on the file system similar to Hive's partitioning scheme. + /// + /// Column names to partition by + /// This DataStreamWriter object + public DataStreamWriter PartitionBy(params string[] colNames) + { + Reference.Invoke("partitionBy", (object)colNames); + return this; + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataStreamWriter object + public DataStreamWriter Option(string key, string value) + { + OptionInternal(key, value); + return this; + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataStreamWriter object + public DataStreamWriter Option(string key, bool value) + { + OptionInternal(key, value); + return this; + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataStreamWriter object + public DataStreamWriter Option(string key, long value) + { + OptionInternal(key, value); + return this; + } + + /// + /// Adds an output option for the underlying data source. + /// + /// Name of the option + /// Value of the option + /// This DataStreamWriter object + public DataStreamWriter Option(string key, double value) + { + OptionInternal(key, value); + return this; + } + + /// + /// Adds output options for the underlying data source. + /// + /// Key/value options + /// This DataStreamWriter object + public DataStreamWriter Options(Dictionary options) + { + Reference.Invoke("options", options); + return this; + } + + /// + /// Sets the trigger for the stream query. + /// + /// Trigger object + /// This DataStreamWriter object + public DataStreamWriter Trigger(Trigger trigger) + { + Reference.Invoke("trigger", trigger); + return this; + } + + /// + /// Specifies the name of the + /// that can be started with `start()`. + /// This name must be unique among all the currently active queries + /// in the associated SQLContext. + /// + /// Query name + /// This DataStreamWriter object + public DataStreamWriter QueryName(string queryName) + { + Reference.Invoke("queryName", queryName); + return this; + } + + /// + /// Starts the execution of the streaming query. + /// + /// Optional output path + /// StreamingQuery object + public StreamingQuery Start(string path = null) + { + if (!string.IsNullOrEmpty(path)) + { + return new StreamingQuery((JvmObjectReference)Reference.Invoke("start", path)); + } + return new StreamingQuery((JvmObjectReference)Reference.Invoke("start")); + } + + /// + /// Starts the execution of the streaming query, which will continually output results to the + /// given table as new data arrives. The returned object can be + /// used to interact with the stream. + /// + /// + /// For v1 table, partitioning columns provided by will be + /// respected no matter the table exists or not. A new table will be created if the table not + /// exists. + /// + /// For v2 table, will be ignored if the table already exists. + /// will be respected only if the v2 table does not exist. + /// Besides, the v2 table created by this API lacks some functionalities (e.g., customized + /// properties, options, and serde info). If you need them, please create the v2 table manually + /// before the execution to avoid creating a table with incomplete information. + /// + /// Name of the table + /// StreamingQuery object + [Since(Versions.V3_1_0)] + public StreamingQuery ToTable(string tableName) + { + return new StreamingQuery((JvmObjectReference)Reference.Invoke("toTable", tableName)); + } + + /// + /// Sets the output of the streaming query to be processed using the provided + /// writer object. See for more details on the + /// lifecycle and semantics. + /// + /// + /// This DataStreamWriter object + [Since(Versions.V2_4_0)] + public DataStreamWriter Foreach(IForeachWriter writer) + { + RDD.WorkerFunction.ExecuteDelegate wrapper = + new ForeachWriterWrapperUdfWrapper( + new ForeachWriterWrapper(writer).Process).Execute; + + Reference.Invoke( + "foreach", + Reference.Jvm.CallConstructor( + "org.apache.spark.sql.execution.python.PythonForeachWriter", + UdfUtils.CreatePythonFunction( + Reference.Jvm, + CommandSerDe.Serialize( + wrapper, + CommandSerDe.SerializedMode.Row, + CommandSerDe.SerializedMode.Row)), + DataType.FromJson(Reference.Jvm, _df.Schema().Json))); + + return this; + } + + /// + /// Sets the output of the streaming query to be processed using the provided + /// function. This is supported only in the micro-batch execution modes (that + /// is, when the trigger is not continuous). In every micro-batch, the provided + /// function will be called in every micro-batch with (i) the output rows as a + /// and (ii) the batch identifier. The batchId can be used + /// to deduplicate and transactionally write the output (that is, the provided + /// Dataset) to external systems. The output is guaranteed + /// to exactly same for the same batchId (assuming all operations are deterministic + /// in the query). + /// + /// The function to apply to the DataFrame + /// This DataStreamWriter object + [Since(Versions.V2_4_0)] + public DataStreamWriter ForeachBatch(Action func) + { + int callbackId = SparkEnvironment.CallbackServer.RegisterCallback( + new ForeachBatchCallbackHandler(Reference.Jvm, func)); + Reference.Jvm.CallStaticJavaMethod( + "org.apache.spark.sql.api.dotnet.DotnetForeachBatchHelper", + "callForeachBatch", + SparkEnvironment.CallbackServer.JvmCallbackClient, + this, + callbackId); + return this; + } + + /// + /// Helper function to add given key/value pair as a new option. + /// + /// Name of the option + /// Value of the option + /// This DataStreamWriter object + private DataStreamWriter OptionInternal(string key, object value) + { + Reference.Invoke("option", key, value); + return this; + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Streaming/OutputMode.cs b/src/spark/Flowthru.Spark/Sql/Streaming/OutputMode.cs new file mode 100644 index 00000000..a79888ae --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Streaming/OutputMode.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Flowthru.Spark.Sql.Streaming +{ + /// + /// Output modes for specifying how data of a streaming DataFrame is written to a + /// streaming sink. + /// + public enum OutputMode + { + /// + /// OutputMode in which only the new rows in the streaming DataFrame/Dataset will be + /// written to the sink. This output mode can be only be used in queries that do not + /// contain any aggregation. + /// + Append, + + /// + /// OutputMode in which all the rows in the streaming DataFrame/Dataset will be written + /// to the sink every time these is some updates. This output mode can only be used in + /// queries that contain aggregations. + /// + Complete, + + /// + /// OutputMode in which only the rows in the streaming DataFrame/Dataset that were updated + /// will be written to the sink every time these is some updates. If the query doesn't + /// contain aggregations, it will be equivalent to Append mode. + /// + Update + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Streaming/StreamingQuery.cs b/src/spark/Flowthru.Spark/Sql/Streaming/StreamingQuery.cs new file mode 100644 index 00000000..d97e6c5c --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Streaming/StreamingQuery.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Internal.Scala; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql.Streaming +{ + /// + /// A handle to a query that is executing continuously in the background as new data arrives. + /// + public sealed class StreamingQuery : IJvmObjectReferenceProvider + { + internal StreamingQuery(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + + /// + /// Returns the user-specified name of the query, or null if not specified. + /// + public string Name => (string)Reference.Invoke("name"); + + /// + /// Returns the unique id of this query that persists across restarts from checkpoint data. + /// That is, this id is generated when a query is started for the first time, and + /// will be the same every time it is restarted from checkpoint data. Also see + /// . + /// + public string Id => + (string)((JvmObjectReference)Reference.Invoke("id")).Invoke("toString"); + + /// + /// Returns the unique id of this run of the query. That is, every start/restart of + /// a query will generated a unique runId. Therefore, every time a query is restarted + /// from checkpoint, it will have the same but different + /// s. + /// + public string RunId => + (string)((JvmObjectReference)Reference.Invoke("runId")).Invoke("toString"); + + /// + /// Returns true if this query is actively running. + /// + /// True if this query is actively running + public bool IsActive() => (bool)Reference.Invoke("isActive"); + + /// + /// Waits for the termination of this query, either by Stop() or by an exception. + /// + public void AwaitTermination() => Reference.Invoke("awaitTermination"); + + /// + /// Returns true if this query is terminated within the timeout in milliseconds. + /// + /// + /// If the query has terminated, then all subsequent calls to this method will either + /// return true immediately (if the query was terminated by Stop()), or throw an + /// exception immediately (if the query has terminated with an exception). + /// + /// Timeout value in milliseconds + /// true if this query is terminated within timeout + public bool AwaitTermination(long timeoutMs) => + (bool)Reference.Invoke("awaitTermination", timeoutMs); + + /// + /// Blocks until all available data in the source has been processed and committed to the + /// sink. This method is intended for testing. Note that in the case of continually + /// arriving data, this method may block forever. Additionally, this method is only + /// guaranteed to block until data that has been synchronously appended data to a + /// `org.apache.spark.sql.execution.streaming.Source` prior to invocation. + /// (i.e. `getOffset` must immediately reflect the addition). + /// + public void ProcessAllAvailable() => Reference.Invoke("processAllAvailable"); + + /// + /// Stops the execution of this query if it is running. This method blocks until the + /// threads performing execution stop. + /// + public void Stop() => Reference.Invoke("stop"); + + /// + /// Prints the physical plan to the console for debugging purposes. + /// + /// Whether to do extended explain or not + public void Explain(bool extended = false) => Reference.Invoke("explain", extended); + + /// + /// The if the query was terminated by an exception, + /// null otherwise. + /// + public StreamingQueryException Exception() + { + var optionalException = new Option((JvmObjectReference)Reference.Invoke("exception")); + if (optionalException.IsDefined()) + { + var exception = (JvmObjectReference)optionalException.Get(); + return new StreamingQueryException((string)exception.Invoke("toString")); + } + + return null; + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Streaming/StreamingQueryException.cs b/src/spark/Flowthru.Spark/Sql/Streaming/StreamingQueryException.cs new file mode 100644 index 00000000..e5c96b3f --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Streaming/StreamingQueryException.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql.Streaming +{ + /// + /// Exception that stopped a . + /// + public class StreamingQueryException : JvmException + { + public StreamingQueryException(string message) + : base(message) + { + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Streaming/StreamingQueryManager.cs b/src/spark/Flowthru.Spark/Sql/Streaming/StreamingQueryManager.cs new file mode 100644 index 00000000..5b24d0c4 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Streaming/StreamingQueryManager.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql.Streaming +{ + /// + /// A class to manage all the active + /// in a . + /// + public sealed class StreamingQueryManager : IJvmObjectReferenceProvider + { + internal StreamingQueryManager(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + + /// + /// Returns a list of active queries associated with this SQLContext. + /// + /// Active queries associated with this SQLContext. + public IEnumerable Active() => + ((JvmObjectReference[])Reference.Invoke("active")) + .Select(sq => new StreamingQuery(sq)); + + /// + /// Returns an active query from this SQLContext or throws exception if an active + /// query with this name doesn't exist. + /// + /// Id of the . + /// + /// if there is an active query with the given id. + /// + public StreamingQuery Get(string id) => + new StreamingQuery((JvmObjectReference)Reference.Invoke("get", id)); + + /// + /// Wait until any of the queries on the associated SQLContext has terminated since the + /// creation of the context, or since was called. If any + /// query was terminated with an exception, then the exception will be thrown. + /// + /// If a query has terminated, then subsequent calls to + /// will either return immediately (if the query was terminated by + /// ), or throw the exception immediately (if the query + /// was terminated with exception). Use to clear past + /// terminations and wait for new terminations. + /// + /// In the case where multiple queries have terminated since + /// was called, if any query has terminated with exception, then + /// will throw any of the exception. For correctly + /// documenting exceptions across multiple queries, users need to stop all of them after + /// any of them terminates with exception, and then check the + /// for each query. + /// + /// Throws StreamingQueryException on the JVM if any query has terminated with an + /// exception. + /// + public void AwaitAnyTermination() => Reference.Invoke("awaitAnyTermination"); + + /// + /// Wait until any of the queries on the associated SQLContext has terminated since the + /// creation of the context, or since was called. If any + /// query was terminated with an exception, then the exception will be thrown. + /// + /// If a query has terminated, then subsequent calls to + /// will either return immediately (if the query was terminated by + /// ), or throw the exception immediately (if the query + /// was terminated with exception). Use to clear past + /// terminations and wait for new terminations. + /// + /// In the case where multiple queries have terminated since + /// was called, if any query has terminated with exception, then + /// will throw any of the exception. For correctly + /// documenting exceptions across multiple queries, users need to stop all of them after + /// any of them terminates with exception, and then check the + /// for each query. + /// + /// Throws StreamingQueryException on the JVM if any query has terminated with an + /// exception. + /// + /// + /// Milliseconds to wait for query to terminate. Returns whether the query has terminated + /// or not. + /// + public void AwaitAnyTermination(long timeoutMs) => + Reference.Invoke("awaitAnyTermination", timeoutMs); + + /// + /// Forget about past terminated queries so that can be + /// used again to wait for new terminations + /// + public void ResetTerminated() => Reference.Invoke("resetTerminated"); + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Streaming/Trigger.cs b/src/spark/Flowthru.Spark/Sql/Streaming/Trigger.cs new file mode 100644 index 00000000..a90cd633 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Streaming/Trigger.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Sql.Streaming +{ + /// + /// Policy used to indicate how often results should be produced by a + /// + /// + public sealed class Trigger : IJvmObjectReferenceProvider + { + private static IJvmBridge Jvm { get; } = SparkEnvironment.JvmBridge; + private static readonly string s_triggerClassName = + "org.apache.spark.sql.streaming.Trigger"; + + internal Trigger(JvmObjectReference jvmObject) => Reference = jvmObject; + + public JvmObjectReference Reference { get; private set; } + + /// + /// A trigger policy that runs a query periodically based on an interval + /// in processing time. + /// If `interval` is 0, the query will run as fast as possible. + /// + /// Milliseconds + /// Trigger Object + public static Trigger ProcessingTime(long intervalMs) + { + return Apply("ProcessingTime", intervalMs); + } + + /// + /// A trigger policy that runs a query periodically based on an interval + /// in processing time. + /// If `interval` is effectively 0, the query will run as fast as possible. + /// + /// string representation for interval. eg. "10 seconds" + /// Trigger Object + public static Trigger ProcessingTime(string interval) + { + return Apply("ProcessingTime", interval); + } + + /// + /// A trigger that process only one batch of data in a streaming query + /// then terminates the query. + /// + /// Trigger Object + public static Trigger Once() + { + return Apply("Once"); + } + + /// + /// A trigger that continuously processes streaming data, + /// asynchronously checkpointing at the specified interval. + /// + /// Milliseconds + /// Trigger Object + public static Trigger Continuous(long intervalMs) + { + return Apply("Continuous", intervalMs); + } + + /// + /// A trigger that continuously processes streaming data, + /// asynchronously checkpointing at the specified interval. + /// + /// string representation for interval. eg. "10 seconds" + /// Trigger Object + public static Trigger Continuous(string interval) + { + return Apply("Continuous", interval); + } + + private static Trigger Apply(string funcName, params object[] args) + { + return new Trigger( + (JvmObjectReference)Jvm.CallStaticJavaMethod( + s_triggerClassName, + funcName, + args)); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Types/ComplexTypes.cs b/src/spark/Flowthru.Spark/Sql/Types/ComplexTypes.cs new file mode 100644 index 00000000..dc96e14e --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Types/ComplexTypes.cs @@ -0,0 +1,313 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Flowthru.Spark.Interop.Ipc; +using Newtonsoft.Json.Linq; + +namespace Flowthru.Spark.Sql.Types +{ + /// + /// An array type containing multiple values of a type. + /// + public sealed class ArrayType : DataType + { + /// + /// Constructor for ArrayType class. + /// + /// The data type of elements in this array + /// Indicates if elements can be null + public ArrayType(DataType elementType, bool containsNull = true) + { + ElementType = elementType; + ContainsNull = containsNull; + } + + /// + /// Constructor for ArrayType class. + /// + /// JSON object to create the array type from + internal ArrayType(JObject json) => FromJson(json); + + /// + /// Returns the data type of the elements in an array. + /// + public DataType ElementType { get; private set; } + + /// + /// Checks if the array can contain null values. + /// + public bool ContainsNull { get; private set; } + + /// + /// Readable string representation for this type. + /// + public override string SimpleString => + string.Format("array<{0}>", ElementType.SimpleString); + + /// + /// Returns JSON object describing this type. + /// + internal override object JsonValue => + new JObject( + new JProperty("type", TypeName), + new JProperty("elementType", ElementType.JsonValue), + new JProperty("containsNull", ContainsNull)); + + /// + /// Constructs a ArrayType object from a JSON object. + /// + /// JSON object used to construct a ArrayType object + /// ArrayType object + private DataType FromJson(JObject json) + { + ElementType = ParseDataType(json["elementType"]); + ContainsNull = (bool)json["containsNull"]; + return this; + } + + internal override bool NeedConversion() => ElementType.NeedConversion(); + + internal override object FromInternal(object obj) + { + if (!NeedConversion() || obj == null) + { + return obj; + } + + var arrayList = (ArrayList)obj; + for (int i = 0; i < arrayList.Count; ++i) + { + arrayList[i] = ElementType.FromInternal(arrayList[i]); + } + + return arrayList; + } + } + + /// + /// The data type for a map. + /// + public sealed class MapType : DataType + { + /// + /// Constructor for MapType class. + /// + /// The data type of keys in this map + /// The data type of values in this map + /// Indicates if values can be null + public MapType(DataType keyType, DataType valueType, bool valueContainsNull = true) + { + KeyType = keyType; + ValueType = valueType; + ValueContainsNull = valueContainsNull; + } + + /// + /// Constructor for MapType class. + /// + /// JSON object to create the map type from + internal MapType(JObject json) => FromJson(json); + + /// + /// Returns the data type of the keys in the map. + /// + public DataType KeyType { get; private set; } + + /// + /// Returns the data type of the values in the map. + /// + public DataType ValueType { get; private set; } + + /// + /// Checks if the value can contain null values. + /// + public bool ValueContainsNull { get; private set; } + + /// + /// Readable string representation for this type. + /// + public override string SimpleString => + string.Format("map<{0},{1}>", KeyType.SimpleString, ValueType.SimpleString); + + /// + /// Returns JSON object describing this type. + /// + internal override object JsonValue => + new JObject( + new JProperty("type", TypeName), + new JProperty("keyType", KeyType.JsonValue), + new JProperty("valueType", ValueType.JsonValue), + new JProperty("valueContainsNull", ValueContainsNull)); + + /// + /// Constructs a MapType object from a JSON object. + /// + /// JSON object used to construct a MapType object + /// MapType object + private DataType FromJson(JObject json) + { + KeyType = ParseDataType(json["keyType"]); + ValueType = ParseDataType(json["valueType"]); + ValueContainsNull = (bool)json["valueContainsNull"]; + return this; + } + + internal override bool NeedConversion() => + KeyType.NeedConversion() || ValueType.NeedConversion(); + + internal override object FromInternal(object obj) + { + if (!NeedConversion() || obj == null) + { + return obj; + } + + var hashTable = (Hashtable)obj; + var convertedHashtable = new Hashtable(hashTable.Count); + foreach (DictionaryEntry entry in hashTable) + { + convertedHashtable[KeyType.FromInternal(entry.Key)] = + ValueType.FromInternal(entry.Value); + } + + return convertedHashtable; + } + } + + /// + /// A type that represents a field inside StructType. + /// + public sealed class StructField + { + /// + /// Constructor for StructFieldType class. + /// + /// The name of this field + /// The data type of this field + /// Indicates if values of this field can be null + /// The metadata of this field + public StructField( + string name, + DataType dataType, + bool isNullable = true, + JObject metadata = null) + { + Name = name; + DataType = dataType; + IsNullable = isNullable; + Metadata = metadata ?? new JObject(); + } + + /// + /// Constructor for StructFieldType class. + /// + /// JSON object to construct a StructFieldType object + internal StructField(JObject json) + { + Name = json["name"].ToString(); + DataType = DataType.ParseDataType(json["type"]); + IsNullable = (bool)json["nullable"]; + Metadata = (JObject)json["metadata"]; + } + + /// + /// The name of this field. + /// + public string Name { get; } + + /// + /// The data type of this field. + /// + public DataType DataType { get; } + + /// + /// Checks if values of this field can be null. + /// + public bool IsNullable { get; } + + /// + /// The metadata of this field. + /// + internal JObject Metadata { get; } + + /// + /// Returns a readable string that represents this type. + /// + public override string ToString() => $"StructField({Name},{DataType.SimpleString})"; + + /// + /// Returns JSON object describing this type. + /// + internal object JsonValue => + new JObject( + new JProperty("name", Name), + new JProperty("type", DataType.JsonValue), + new JProperty("nullable", IsNullable), + new JProperty("metadata", Metadata)); + } + + /// + /// Struct type represents a struct with multiple fields. + /// This type is also used to represent a Row object in Spark. + /// + public sealed class StructType : DataType + { + /// + /// Constructor for StructType class. + /// + /// JSON object to construct a StructType object + internal StructType(JObject json) => FromJson(json); + + /// + /// Constructor for StructType class. + /// + /// StructType object on JVM + internal StructType(JvmObjectReference jvmObject) => + FromJson(JObject.Parse((string)jvmObject.Invoke("json"))); + + /// + /// Returns a list of StructFieldType objects. + /// + public List Fields { get; private set; } + + /// + /// Constructor for StructType class. + /// + /// A collection of StructFieldType objects + public StructType(IEnumerable fields) + { + Fields = fields.ToList(); + } + + /// + /// Returns a readable string that represents this type. + /// + public override string SimpleString => + $"struct<{string.Join(",", Fields.Select(f => $"{f.Name}:{f.DataType.SimpleString}"))}>"; + + /// + /// Returns JSON object describing this type. + /// + internal override object JsonValue => + new JObject( + new JProperty("type", TypeName), + new JProperty("fields", Fields.Select(f => f.JsonValue).ToArray())); + + /// + /// Constructs a StructType object from a JSON object + /// + /// JSON object used to construct a StructType object + /// A StuructType object + private DataType FromJson(JObject json) + { + IEnumerable fieldsJObjects = json["fields"].Select(f => (JObject)f); + Fields = fieldsJObjects.Select( + fieldJObject => new StructField(fieldJObject)).ToList(); + return this; + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Types/DataType.cs b/src/spark/Flowthru.Spark/Sql/Types/DataType.cs new file mode 100644 index 00000000..651d45d1 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Types/DataType.cs @@ -0,0 +1,238 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using Flowthru.Spark.Interop.Ipc; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Flowthru.Spark.Sql.Types +{ + /// + /// The base type of all Spark SQL data types. + /// Note that the implementation mirrors PySpark: spark/python/pyspark/sql/types.py + /// The Scala version is spark/sql/catalyst/src/main/scala/org/apache/spark/sql/types/*. + /// + public abstract class DataType + { + private static readonly Type[] s_simpleTypes = new[] { + typeof(NullType), + typeof(StringType), + typeof(BinaryType), + typeof(BooleanType), + typeof(DateType), + typeof(TimestampType), + typeof(DoubleType), + typeof(FloatType), + typeof(ByteType), + typeof(IntegerType), + typeof(LongType), + typeof(ShortType), + typeof(DecimalType) }; + + private static readonly Type[] s_complexTypes = new[] { + typeof(ArrayType), + typeof(MapType), + typeof(StructType) }; + + private static readonly Lazy s_simpleTypeNormalizedNames = + new Lazy( + () => s_simpleTypes.Select(t => NormalizeTypeName(t.Name)).ToArray()); + + private static readonly Lazy s_complexTypeNormalizedNames = + new Lazy( + () => s_complexTypes.Select(t => NormalizeTypeName(t.Name)).ToArray()); + + private string _typeName; + + /// + /// Normalized type name. + /// + public string TypeName => _typeName ??= NormalizeTypeName(GetType().Name); + + /// + /// Simple string version of the current data type. + /// + public virtual string SimpleString => TypeName; + + /// + /// The compact JSON representation of this data type. + /// + public string Json + { + get + { + object jObject = (JsonValue is JObject) ? + ((JObject)JsonValue).SortProperties() : + JsonValue; + return JsonConvert.SerializeObject(jObject, Formatting.None); + } + } + + /// + /// JSON value of this data type. + /// + internal virtual object JsonValue => TypeName; + + /// + /// Parses a JSON string to create a . + /// It references a on the JVM side. + /// + /// JVM bridge to use + /// JSON string to parse + /// The new JvmObjectReference created from the JSON string + internal static JvmObjectReference FromJson(IJvmBridge jvm, string json) + { + return (JvmObjectReference)jvm.CallStaticJavaMethod( + "org.apache.spark.sql.types.DataType", + "fromJson", + json); + } + + /// + /// Parses a JSON string to construct a DataType. + /// + /// JSON string to parse + /// The new DataType instance from the JSON string + public static DataType ParseDataType(string json) => ParseDataType(JToken.Parse(json)); + + /// + /// Checks if the given object is same as the current object by + /// checking the string version of this type. + /// + /// Other object to compare against + /// True if the other object is equal. + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is DataType otherDataType) + { + return SimpleString == otherDataType.SimpleString; + } + + return false; + } + + /// + /// Returns the hash code of the current object. + /// + /// The hash code of the current object + public override int GetHashCode() => SimpleString.GetHashCode(); + + /// + /// Parses a JToken object to construct a DataType. + /// + /// JToken object to parse + /// The new DataType instance from the JSON string + internal static DataType ParseDataType(JToken json) + { + if (json.Type == JTokenType.Object) + { + var typeJObject = (JObject)json; + if (typeJObject.TryGetValue("type", out JToken type)) + { + string typeName = type.ToString(); + + int typeIndex = Array.IndexOf(s_complexTypeNormalizedNames.Value, typeName); + + if (typeIndex != -1) + { + return (DataType)Activator.CreateInstance( + s_complexTypes[typeIndex], + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + null, + new object[] { typeJObject }, + null); + } + else if (typeName == "udt") + { + if (typeJObject.TryGetValue("class", out JToken classToken)) + { + if (typeJObject.TryGetValue("sqlType", out JToken sqlTypeToken)) + { + return new StructType((JObject)sqlTypeToken); + } + } + + throw new NotImplementedException(); + } + } + + throw new ArgumentException($"Could not parse data type: {type}"); + } + else + { + return ParseSimpleType(json); + } + + } + + /// + /// Does this type need to conversion between C# object and internal SQL object. + /// + internal virtual bool NeedConversion() => false; + + /// + /// Converts an internal SQL object into a native C# object. + /// + internal virtual object FromInternal(object obj) => obj; + + /// + /// Parses a JToken object that represents a simple type. + /// + /// JToken object to parse + /// The new DataType instance from the JSON string + private static DataType ParseSimpleType(JToken json) + { + string typeName = json.ToString(); + + int typeIndex = Array.IndexOf(s_simpleTypeNormalizedNames.Value, typeName); + + if (typeIndex != -1) + { + return (DataType)Activator.CreateInstance(s_simpleTypes[typeIndex]); + } + + Match decimalMatch = DecimalType.s_fixedDecimal.Match(typeName); + if (decimalMatch.Success) + { + return new DecimalType( + int.Parse(decimalMatch.Groups[1].Value), + int.Parse(decimalMatch.Groups[2].Value)); + } + + throw new ArgumentException($"Could not parse data type: {json}"); + } + + /// + /// Remove "Type" from the end of type name and lower cases to align with Scala type name. + /// + /// Type name to normalize + /// Normalized type name + private static string NormalizeTypeName(string typeName) + { +#if !NETSTANDARD2_0 + return string.Create(typeName.Length - 4, typeName, (span, name) => + { + name.AsSpan(0, name.Length - 4).ToLower(span, CultureInfo.CurrentCulture); + }); +#else + return typeName.Substring(0, typeName.Length - 4).ToLower(); +#endif + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Types/Date.cs b/src/spark/Flowthru.Spark/Sql/Types/Date.cs new file mode 100644 index 00000000..74885191 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Types/Date.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Flowthru.Spark.Sql.Types +{ + /// + /// Represents Date containing year, month, and day. + /// + public class Date + { + private readonly DateTime _dateTime; + + /// + /// Constructor for Date class. + /// + /// DateTime object + public Date(DateTime dateTime) + { + _dateTime = dateTime; + } + + /// + /// Constructor for Date class. + /// + /// The year (1 through 9999) + /// The month (1 through 12) + /// The day (1 through the number of days in month) + public Date(int year, int month, int day) + { + _dateTime = new DateTime(year, month, day); + } + + /// + /// Returns the year component of the date. + /// + public int Year => _dateTime.Year; + + /// + /// Returns the month component of the date. + /// + public int Month => _dateTime.Month; + + /// + /// Returns the day component of the date. + /// + public int Day => _dateTime.Day; + + /// + /// Readable string representation for this type. + /// + public override string ToString() => _dateTime.ToString("yyyy-MM-dd"); + + /// + /// Checks if the given object is same as the current object. + /// + /// Other object to compare against + /// True if the other object is equal + public override bool Equals(object obj) => + ReferenceEquals(this, obj) || + ((obj is Date date) && Year.Equals(date.Year) && Month.Equals(date.Month) && + Day.Equals(date.Day)); + + /// + /// Returns the hash code of the current object. + /// + /// The hash code of the current object + public override int GetHashCode() => base.GetHashCode(); + + /// + /// Returns DateTime object describing this type. + /// + /// DateTime object of the current object + public DateTime ToDateTime() => _dateTime; + + /// + /// Returns an integer object that represents a count of days from 1970-01-01. + /// + /// Integer object that represents a count of days from 1970-01-01 + internal int GetInterval() => (_dateTime - DateType.s_unixTimeEpoch).Days; + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Types/SimpleTypes.cs b/src/spark/Flowthru.Spark/Sql/Types/SimpleTypes.cs new file mode 100644 index 00000000..01c3c1e5 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Types/SimpleTypes.cs @@ -0,0 +1,199 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text.RegularExpressions; + +namespace Flowthru.Spark.Sql.Types +{ + /// + /// An internal type used to represent everything that is not null, arrays, structs, and maps. + /// + public abstract class AtomicType : DataType + { + } + + /// + /// Represents a numeric type. + /// + public abstract class NumericType : AtomicType + { + } + + /// + /// Represents an integral type. + /// + public abstract class IntegralType : NumericType + { + } + + /// + /// Represents a fractional type. + /// + public abstract class FractionalType : NumericType + { + } + + /// + /// Represents a null type. + /// + public sealed class NullType : DataType + { + } + + /// + /// Represents a string type. + /// + public sealed class StringType : AtomicType + { + } + + /// + /// Represents a binary (byte array) type. + /// + public sealed class BinaryType : AtomicType + { + } + + /// + /// Represents a boolean type. + /// + public sealed class BooleanType : AtomicType + { + } + + /// + /// Represents a date type. It represents a valid date in the proleptic Gregorian + /// calendar. Valid range is [0001-01-01, 9999-12-31]. + /// + public sealed class DateType : AtomicType + { + internal static readonly DateTime s_unixTimeEpoch = + new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + internal override bool NeedConversion() => true; + + /// + /// Internally, a date is stored as a simple incrementing count of days as int + /// where day 0 is 1970-01-01. This will convert internal SQL DateType objects + /// from count of days into native C# Date objects. + /// + internal override object FromInternal(object obj) + { + if (obj == null) + { + return null; + } + + return new Date(new DateTime((int)obj * TimeSpan.TicksPerDay + s_unixTimeEpoch.Ticks)); + } + } + + /// + /// Represents a timestamp type. It represents a time instant in microsecond precision. + /// Valid range is [0001-01-01T00:00:00.000000Z, 9999-12-31T23:59:59.999999Z] where + /// the left/right-bound is a date and time of the proleptic Gregorian calendar in UTC+00:00. + /// + public sealed class TimestampType : AtomicType + { + internal override bool NeedConversion() => true; + + /// + /// Internally, a timestamp is stored as the number of microseconds as long from the epoch + /// of 1970-01-01T00:00:00.000000Z(UTC+00:00). This will convert internal SQL TimestampType + /// objects from the number of microseconds into native C# Timestamp objects. + /// + internal override object FromInternal(object obj) + { + if (obj == null) + { + return null; + } + + // Known issue that if the original type is "long" and its value can be fit into the + // "int", Pickler will serialize the value as int. + long val = (obj is long v) ? v : (int)obj; + return new Timestamp( + new DateTime(val * 10 + DateType.s_unixTimeEpoch.Ticks, DateTimeKind.Utc)); + } + } + + /// + /// Represents a double type. + /// + public sealed class DoubleType : FractionalType + { + } + + /// + /// Represents a float type. + /// + public sealed class FloatType : FractionalType + { + } + + /// + /// Represents a byte type. + /// + public sealed class ByteType : IntegralType + { + } + + /// + /// Represents an int type. + /// + public sealed class IntegerType : IntegralType + { + } + + /// + /// Represents a long type. + /// + public sealed class LongType : IntegralType + { + } + + /// + /// Represents a short type. + /// + public sealed class ShortType : IntegralType + { + } + + /// + /// Represents a decimal type. + /// + public sealed class DecimalType : FractionalType + { + internal static Regex s_fixedDecimal = + new Regex(@"decimal\(\s*(\d+)\s*,\s*(\-?\d+)\s*\)", RegexOptions.Compiled); + + private readonly int _precision; + private readonly int _scale; + + /// + /// Initializes the instance. + /// + /// + /// Default values of precision and scale are from Scala: + /// sql/catalyst/src/main/scala/org/apache/spark/sql/types/DecimalType.scala. + /// + /// Number of digits in a number + /// + /// Number of digits to the right of the decimal point in a number + /// + public DecimalType(int precision = 10, int scale = 0) + { + _precision = precision; + _scale = scale; + } + + /// + /// Returns simple string version of DecimalType. + /// + public override string SimpleString => $"decimal({_precision},{_scale})"; + + internal override object JsonValue => SimpleString; + } +} diff --git a/src/spark/Flowthru.Spark/Sql/Types/Timestamp.cs b/src/spark/Flowthru.Spark/Sql/Types/Timestamp.cs new file mode 100644 index 00000000..895add7f --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/Types/Timestamp.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Flowthru.Spark.Sql.Types +{ + /// + /// Represents Timestamp containing year, month, day, hour, minute, second, microsecond in + /// Coordinated Universal Time (UTC). + /// + public class Timestamp + { + private readonly DateTime _dateTime; + + /// + /// Constructor for Timestamp class. + /// + /// DateTime object + public Timestamp(DateTime dateTime) + { + _dateTime = dateTime.ToUniversalTime(); + } + + /// + /// Constructor for Timestamp class, the defaut timezone is + /// Coordinated Universal Time (UTC). + /// + /// The year (1 through 9999) + /// The month (1 through 12) + /// The day (1 through the number of days in month) + /// The hour (0 through 23) + /// The minute (0 through 59) + /// The second (0 through 59) + /// The microsecond (0 through 999999) + public Timestamp( + int year, + int month, + int day, + int hour, + int minute, + int second, + int microsecond) + { + if ((0 <= microsecond) && (microsecond <= 999999)) + { + // Create DateTime and AddTicks based on the microsecond value. + _dateTime = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Utc) + .AddTicks(microsecond * 10); + } + else + { + throw new ArgumentOutOfRangeException($"Invalid microsecond value {microsecond}. " + + $"The microsecond should be in the range of [0, 999999]."); + } + } + + /// + /// Returns the year component of the timestamp. + /// + public int Year => _dateTime.Year; + + /// + /// Returns the month component of the timestamp. + /// + public int Month => _dateTime.Month; + + /// + /// Returns the day component of the timestamp. + /// + public int Day => _dateTime.Day; + + /// + /// Returns the hour component of the timestamp. + /// + public int Hour => _dateTime.Hour; + + /// + /// Returns the minute component of the timestamp. + /// + public int Minute => _dateTime.Minute; + + /// + /// Returns the second component of the timestamp. + /// + public int Second => _dateTime.Second; + + /// + /// Returns the microsecond component of the timestamp. + /// + public int Microsecond => (int)(_dateTime.Ticks % 10000000 / 10); + + /// + /// Readable string representation for this type. + /// + public override string ToString() => _dateTime.ToString("yyyy-MM-dd HH:mm:ss.ffffffZ"); + + /// + /// Checks if the given object is same as the current object. + /// + /// Other object to compare against + /// True if the other object is equal + public override bool Equals(object obj) => + ReferenceEquals(this, obj) || + ((obj is Timestamp timestamp) && _dateTime.Equals(timestamp._dateTime)); + + /// + /// Returns the hash code of the current object. + /// + /// The hash code of the current object + public override int GetHashCode() => _dateTime.GetHashCode(); + + /// + /// Returns DateTime object describing this type. + /// + /// DateTime object of the current object + public DateTime ToDateTime() => _dateTime; + + /// + /// Returns a double object that represents the number of microseconds from the epoch + /// of 1970-01-01T00:00:00.000000Z(UTC+00:00) in the second unit to serialize and + /// deserialize between CLR and JVM. + /// + /// Double object that represents the number of seconds from the epoch of + /// 1970-01-01T00:00:00.000000Z(UTC+00:00) + internal double GetIntervalInSeconds() => + (_dateTime.Ticks - DateType.s_unixTimeEpoch.Ticks) / 10000000.0; + + /// + /// Returns a long object that represents the number of microseconds from the epoch of + /// 1970-01-01T00:00:00.000000Z(UTC+00:00). + /// + /// Long object that represents the number of microseconds from the epoch of + /// 1970-01-01T00:00:00.000000Z(UTC+00:00) + internal long GetIntervalInMicroseconds() => + (_dateTime.Ticks - DateType.s_unixTimeEpoch.Ticks) / 10; + } +} diff --git a/src/spark/Flowthru.Spark/Sql/UDFRegistration.cs b/src/spark/Flowthru.Spark/Sql/UDFRegistration.cs new file mode 100644 index 00000000..f5581289 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/UDFRegistration.cs @@ -0,0 +1,500 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql.Expressions; +using Flowthru.Spark.Sql.Types; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Sql +{ + /// + /// Functions for registering user-defined functions. + /// + public sealed class UdfRegistration : IJvmObjectReferenceProvider + { + internal UdfRegistration(JvmObjectReference jvmObject) + { + Reference = jvmObject; + } + + public JvmObjectReference Reference { get; private set; } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the return type of the UDF. + /// The UDF name. + /// The UDF function implementation. + public void Register(string name, Func f) + { + Register(name, UdfUtils.CreateUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF name. + /// The UDF function implementation. + public void Register(string name, Func f) + { + Register(name, UdfUtils.CreateUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF name. + /// The UDF function implementation. + public void Register(string name, Func f) + { + Register(name, UdfUtils.CreateUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF name. + /// The UDF function implementation. + public void Register(string name, Func f) + { + Register(name, UdfUtils.CreateUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF name. + /// The UDF function implementation. + public void Register(string name, Func f) + { + Register(name, UdfUtils.CreateUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF name. + /// The UDF function implementation. + public void Register( + string name, + Func f) + { + Register(name, UdfUtils.CreateUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF name. + /// The UDF function implementation. + public void Register( + string name, + Func f) + { + Register(name, UdfUtils.CreateUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF name. + /// The UDF function implementation. + public void Register( + string name, + Func f) + { + Register(name, UdfUtils.CreateUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF name. + /// The UDF function implementation. + public void Register( + string name, + Func f) + { + Register(name, UdfUtils.CreateUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF name. + /// The UDF function implementation. + public void Register( + string name, + Func f) + { + Register(name, UdfUtils.CreateUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the type of the tenth argument to the UDF. + /// Specifies the return type of the UDF. + /// The UDF name. + /// The UDF function implementation. + public void Register( + string name, + Func f) + { + Register(name, UdfUtils.CreateUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// The UDF name. + /// The UDF function implementation. + /// Schema associated with this row + public void Register(string name, Func f, StructType returnType) + { + Register(name, UdfUtils.CreateUdfWrapper(f), returnType); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// The UDF name. + /// The UDF function implementation. + /// Schema associated with this row + public void Register(string name, Func f, StructType returnType) + { + Register(name, UdfUtils.CreateUdfWrapper(f), returnType); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// The UDF name. + /// The UDF function implementation. + /// Schema associated with this row + public void Register( + string name, + Func f, + StructType returnType) + { + Register(name, UdfUtils.CreateUdfWrapper(f), returnType); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// The UDF name. + /// The UDF function implementation. + /// Schema associated with this row + public void Register( + string name, + Func f, + StructType returnType) + { + Register(name, UdfUtils.CreateUdfWrapper(f), returnType); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// The UDF name. + /// The UDF function implementation. + /// Schema associated with this row + public void Register( + string name, + Func f, + StructType returnType) + { + Register(name, UdfUtils.CreateUdfWrapper(f), returnType); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// The UDF name. + /// The UDF function implementation. + /// Schema associated with this row + public void Register( + string name, + Func f, + StructType returnType) + { + Register(name, UdfUtils.CreateUdfWrapper(f), returnType); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// The UDF name. + /// The UDF function implementation. + /// Schema associated with this row + public void Register( + string name, + Func f, + StructType returnType) + { + Register(name, UdfUtils.CreateUdfWrapper(f), returnType); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// The UDF name. + /// The UDF function implementation. + /// Schema associated with this row + public void Register( + string name, + Func f, + StructType returnType) + { + Register(name, UdfUtils.CreateUdfWrapper(f), returnType); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// The UDF name. + /// The UDF function implementation. + /// Schema associated with this row + public void Register( + string name, + Func f, + StructType returnType) + { + Register(name, UdfUtils.CreateUdfWrapper(f), returnType); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// The UDF name. + /// The UDF function implementation. + /// Schema associated with this row + public void Register( + string name, + Func f, + StructType returnType) + { + Register(name, UdfUtils.CreateUdfWrapper(f), returnType); + } + + /// + /// Registers the given delegate as a user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the type of the tenth argument to the UDF. + /// The UDF name. + /// The UDF function implementation. + /// Schema associated with this row + public void Register( + string name, + Func f, + StructType returnType) + { + Register(name, UdfUtils.CreateUdfWrapper(f), returnType); + } + + /// + /// Register a Java UDF class using reflection. + /// + /// Return type + /// Name of the UDF + /// Class name that defines UDF + public void RegisterJava(string name, string className) + { + Reference.Invoke("registerJava", name, className, GetDataType()); + } + + /// + /// Register a Java UDAF class using reflection. + /// + /// Name of the UDAF + /// Class name that defines UDAF + public void RegisterJavaUDAF(string name, string className) + { + Reference.Invoke("registerJavaUDAF", name, className); + } + + /// + /// Helper function to register wrapped udf. + /// + /// Return type of the udf + /// Name of the udf + /// Wrapped UDF function + private void Register(string name, Delegate func) + { + Register(name, func, UdfUtils.PythonEvalType.SQL_BATCHED_UDF); + } + + /// + /// Helper function to register wrapped udf. + /// + /// Name of the udf + /// Wrapped UDF function + /// Schema associated with the udf + private void Register(string name, Delegate func, StructType returnType) + { + Register(name, func, UdfUtils.PythonEvalType.SQL_BATCHED_UDF, returnType.Json); + } + + /// + /// Helper function to register wrapped udf. + /// + /// Return type of the udf + /// Name of the udf + /// Wrapped UDF function + /// The EvalType of the function + internal void Register(string name, Delegate func, UdfUtils.PythonEvalType evalType) + { + Register(name, func, evalType, UdfUtils.GetReturnType(typeof(TResult))); + } + + /// + /// Helper function to register wrapped udf. + /// + /// Name of the udf + /// Wrapped UDF function + /// The EvalType of the function + /// The return type of the function in JSON format + private void Register(string name, Delegate func, UdfUtils.PythonEvalType evalType, string returnType) + { + byte[] command = CommandSerDe.Serialize( + func, + CommandSerDe.SerializedMode.Row, + CommandSerDe.SerializedMode.Row); + + UserDefinedFunction udf = UserDefinedFunction.Create( + Reference.Jvm, + name, + command, + evalType, + returnType); + + Reference.Invoke("registerPython", name, udf); + } + + private JvmObjectReference GetDataType() + { + return DataType.FromJson(Reference.Jvm, UdfUtils.GetReturnType(typeof(T))); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/UdfRegistrationExtensions.cs b/src/spark/Flowthru.Spark/Sql/UdfRegistrationExtensions.cs new file mode 100644 index 00000000..77a46261 --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/UdfRegistrationExtensions.cs @@ -0,0 +1,271 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Apache.Arrow; +using Flowthru.Spark.Utils; + +namespace Flowthru.Spark.Sql +{ + /// + /// Extension methods for . + /// + public static class UdfRegistrationExtensions + { + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T : IArrowArray + where TResult : IArrowArray + { + RegisterVector(udf, name, UdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : IArrowArray + where T2 : IArrowArray + where TResult : IArrowArray + { + RegisterVector(udf, name, UdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where TResult : IArrowArray + { + RegisterVector(udf, name, UdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where TResult : IArrowArray + { + RegisterVector(udf, name, UdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where TResult : IArrowArray + { + RegisterVector(udf, name, UdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where TResult : IArrowArray + { + RegisterVector(udf, name, UdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where TResult : IArrowArray + { + RegisterVector(udf, name, UdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where TResult : IArrowArray + { + RegisterVector(udf, name, UdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where T9 : IArrowArray + where TResult : IArrowArray + { + RegisterVector(udf, name, UdfUtils.CreateVectorUdfWrapper(f)); + } + + /// + /// Registers the given delegate as a vector user-defined function with the specified name. + /// + /// Specifies the type of the first argument to the UDF. + /// Specifies the type of the second argument to the UDF. + /// Specifies the type of the third argument to the UDF. + /// Specifies the type of the fourth argument to the UDF. + /// Specifies the type of the fifth argument to the UDF. + /// Specifies the type of the sixth argument to the UDF. + /// Specifies the type of the seventh argument to the UDF. + /// Specifies the type of the eighth argument to the UDF. + /// Specifies the type of the ninth argument to the UDF. + /// Specifies the type of the tenth argument to the UDF. + /// Specifies the return type of the UDF. + /// The object to invoke the register the Vector UDF. + /// The UDF name. + /// The UDF function implementation. + public static void RegisterVector( + this UdfRegistration udf, string name, Func f) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where T9 : IArrowArray + where T10 : IArrowArray + where TResult : IArrowArray + { + RegisterVector(udf, name, UdfUtils.CreateVectorUdfWrapper(f)); + } + + private static void RegisterVector(UdfRegistration udf, string name, Delegate func) + { + udf.Register(name, func, UdfUtils.PythonEvalType.SQL_SCALAR_PANDAS_UDF); + } + } +} diff --git a/src/spark/Flowthru.Spark/Sql/WorkerFunction.cs b/src/spark/Flowthru.Spark/Sql/WorkerFunction.cs new file mode 100644 index 00000000..40deb47c --- /dev/null +++ b/src/spark/Flowthru.Spark/Sql/WorkerFunction.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Apache.Arrow; + +namespace Flowthru.Spark.Sql +{ + /// + /// Function that will be executed in the worker. + /// + internal abstract class WorkerFunction + { + } + + /// + /// Function that will be executed in the worker using the Apache Arrow format. + /// + internal sealed class ArrowWorkerFunction : WorkerFunction + { + /// + /// Type of the UDF to run. Refer to .Execute. + /// + /// unpickled data, representing a row + /// offsets to access input + /// + internal delegate IArrowArray ExecuteDelegate( + ReadOnlyMemory input, + int[] argOffsets); + + internal ArrowWorkerFunction(ExecuteDelegate func) + { + Func = func; + } + + internal ExecuteDelegate Func { get; } + + /// + /// Used to chain functions. + /// + internal static ArrowWorkerFunction Chain( + ArrowWorkerFunction innerWorkerFunction, + ArrowWorkerFunction outerWorkerFunction) + { + return new ArrowWorkerFunction( + new WorkerFuncChainHelper( + innerWorkerFunction.Func, + outerWorkerFunction.Func).Execute); + } + + private class WorkerFuncChainHelper + { + private readonly ExecuteDelegate _innerFunc; + private readonly ExecuteDelegate _outerFunc; + + /// + /// The outer function will always take 0 as an offset since there is only one + /// return value from an inner function. + /// + private static readonly int[] s_outerFuncArgOffsets = { 0 }; + + internal WorkerFuncChainHelper(ExecuteDelegate inner, ExecuteDelegate outer) + { + _innerFunc = inner; + _outerFunc = outer; + } + + internal IArrowArray Execute( + ReadOnlyMemory input, + int[] argOffsets) + { + // For chaining, create an array with one element, which is a result from the inner + // function. Only the inner function will expect the given offsets, and the outer + // function will always take 0 as an offset since there is only one value in the + // input. + return _outerFunc( + new[] { _innerFunc(input, argOffsets) }, + s_outerFuncArgOffsets); + } + } + } + + /// + /// Function for Grouped Map Vector UDFs using the Apache Arrow format. + /// + internal sealed class ArrowGroupedMapWorkerFunction : WorkerFunction + { + /// + /// A delegate to invoke a Grouped Map Vector UDF. + /// + /// The input data frame. + /// The resultant data frame. + internal delegate RecordBatch ExecuteDelegate(RecordBatch input); + + internal ArrowGroupedMapWorkerFunction(ExecuteDelegate func) + { + Func = func; + } + + internal ExecuteDelegate Func { get; } + } + + /// + /// Function that will be executed in the worker using the Python pickling format. + /// + internal sealed class PicklingWorkerFunction : WorkerFunction + { + /// + /// Type of the UDF to run. Refer to .Execute. + /// + /// split id for the current task + /// unpickled data, representing a row + /// offsets to access input + /// + internal delegate object ExecuteDelegate(int splitId, object[] input, int[] argOffsets); + + internal PicklingWorkerFunction(ExecuteDelegate func) + { + Func = func; + } + + internal ExecuteDelegate Func { get; } + + /// + /// Used to chain functions. + /// + internal static PicklingWorkerFunction Chain( + PicklingWorkerFunction innerWorkerFunction, + PicklingWorkerFunction outerWorkerFunction) + { + return new PicklingWorkerFunction( + new WorkerFuncChainHelper( + innerWorkerFunction.Func, + outerWorkerFunction.Func).Execute); + } + + private class WorkerFuncChainHelper + { + private readonly ExecuteDelegate _innerFunc; + private readonly ExecuteDelegate _outerFunc; + + /// + /// The outer function will always take 0 as an offset since there is only one + /// return value from an inner function. + /// + private static readonly int[] s_outerFuncArgOffsets = { 0 }; + + internal WorkerFuncChainHelper(ExecuteDelegate inner, ExecuteDelegate outer) + { + _innerFunc = inner; + _outerFunc = outer; + } + + internal object Execute(int splitId, object input, int[] argOffsets) + { + // For chaining, create an array with one element, which is a result from the inner + // function. Only the inner function will expect the given offsets, and the outer + // function will always take 0 as an offset since there is only one value in the + // input. + return _outerFunc( + splitId, + new[] { _innerFunc(splitId, (object[])input, argOffsets) }, + s_outerFuncArgOffsets); + } + } + } +} diff --git a/src/spark/Flowthru.Spark/TaskContext.cs b/src/spark/Flowthru.Spark/TaskContext.cs new file mode 100644 index 00000000..cebd07b7 --- /dev/null +++ b/src/spark/Flowthru.Spark/TaskContext.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Flowthru.Spark +{ + /// + /// TaskContext stores information related to a task. + /// + internal class TaskContext + { + internal int StageId { get; set; } + + internal int PartitionId { get; set; } + + internal int AttemptNumber { get; set; } + + internal long AttemptId { get; set; } + + internal int CPUs { get; set; } + + internal bool IsBarrier { get; set; } + + internal int Port { get; set; } + + internal string Secret { get; set; } + + internal IEnumerable Resources { get; set; } = new List(); + + internal Dictionary LocalProperties { get; set; } = + new Dictionary(); + + public override bool Equals(object obj) + { + if (!(obj is TaskContext other)) + { + return false; + } + + return (StageId == other.StageId) && + (PartitionId == other.PartitionId) && + (AttemptNumber == other.AttemptNumber) && + (AttemptId == other.AttemptId) && + Resources.SequenceEqual(other.Resources) && + (LocalProperties.Count == other.LocalProperties.Count) && + !LocalProperties.Except(other.LocalProperties).Any(); + } + + public override int GetHashCode() + { + return StageId; + } + + internal class Resource + { + internal string Key { get; set; } + internal string Value { get; set; } + internal IEnumerable Addresses { get; set; } = new List(); + + public override bool Equals(object obj) + { + if (!(obj is Resource other)) + { + return false; + } + + return (Key == other.Key) && + (Value == other.Value) && + Addresses.SequenceEqual(Addresses); + } + + public override int GetHashCode() + { + return Key.GetHashCode(); + } + } + } + + // TaskContextHolder contains the TaskContext for the current Thread. + internal static class TaskContextHolder + { + // Multiple Tasks can be assigned to a Worker process. Each + // Task will run in its own thread until completion. Therefore + // we set this field as a thread local variable, where each + // thread will have its own copy of the TaskContext. + [ThreadStatic] + internal static TaskContext s_taskContext; + + internal static TaskContext Get() => s_taskContext; + + internal static void Set(TaskContext tc) => s_taskContext = tc; + } +} diff --git a/src/spark/Flowthru.Spark/Utils/AssemblyInfoProvider.cs b/src/spark/Flowthru.Spark/Utils/AssemblyInfoProvider.cs new file mode 100644 index 00000000..403e6442 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/AssemblyInfoProvider.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Net; +using System.Reflection; + +namespace Flowthru.Spark.Utils +{ + /// + /// Provides the for the "Flowthru.Spark" assembly + /// within the current execution context. + /// + internal static class AssemblyInfoProvider + { + private const string FlowthruSparkAssemblyName = "Flowthru.Spark"; + + private static readonly Lazy s_microsoftSparkAssemblyInfo = + new Lazy(() => CreateAssemblyInfo(FlowthruSparkAssemblyName)); + + internal static AssemblyInfo MicrosoftSparkAssemblyInfo() => s_microsoftSparkAssemblyInfo.Value; + + private static AssemblyInfo CreateAssemblyInfo(string assemblyName) + { + Assembly assembly = AppDomain + .CurrentDomain.GetAssemblies() + .Single(asm => asm.GetName().Name == assemblyName); + + AssemblyName asmName = assembly.GetName(); + return new AssemblyInfo + { + AssemblyName = asmName.Name, + AssemblyVersion = asmName.Version.ToString(), + HostName = Dns.GetHostName(), + }; + } + + internal class AssemblyInfo + { + internal string AssemblyName { get; set; } + internal string AssemblyVersion { get; set; } + internal string HostName { get; set; } + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/AssemblyLoader.cs b/src/spark/Flowthru.Spark/Utils/AssemblyLoader.cs new file mode 100644 index 00000000..c26dbdbf --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/AssemblyLoader.cs @@ -0,0 +1,231 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Flowthru.Spark.Services; + +namespace Flowthru.Spark.Utils +{ + internal static class AssemblySearchPathResolver + { + internal const string AssemblySearchPathsEnvVarName = "DOTNET_ASSEMBLY_SEARCH_PATHS"; + + /// + /// Returns the paths to search when loading assemblies in the following order of + /// precedence: + /// 1) Comma-separated paths specified in DOTNET_ASSEMBLY_SEARCH_PATHS environment + /// variable. Note that if a path starts with ".", the working directory will be prepended. + /// 2) The path of the files added through + /// . + /// 3) The working directory. + /// 4) The directory of the application. + /// + /// + /// The reason that the working directory has higher precedence than the directory + /// of the application is for cases when spark is launched on YARN. The executors are run + /// inside 'containers' and files that are passed via 'spark-submit --files' will be pushed + /// to these 'containers'. This path is the working directory and the 1st probing path that + /// will be checked. + /// + /// Assembly search paths + internal static string[] GetAssemblySearchPaths() + { + var searchPaths = new List(); + string searchPathsStr = + Environment.GetEnvironmentVariable(AssemblySearchPathsEnvVarName); + + if (!string.IsNullOrEmpty(searchPathsStr)) + { + foreach (string searchPath in searchPathsStr.Split(',')) + { + string trimmedSearchPath = searchPath.Trim(); + if (trimmedSearchPath.StartsWith(".")) + { + searchPaths.Add( + Path.Combine(Directory.GetCurrentDirectory(), trimmedSearchPath)); + } + else + { + searchPaths.Add(trimmedSearchPath); + } + } + } + + string sparkFilesPath = SparkFiles.GetRootDirectory(); + if (!string.IsNullOrWhiteSpace(sparkFilesPath)) + { + searchPaths.Add(sparkFilesPath); + } + + searchPaths.Add(Directory.GetCurrentDirectory()); + searchPaths.Add(AppDomain.CurrentDomain.BaseDirectory); + + return searchPaths.ToArray(); + } + } + + internal static class AssemblyLoader + { + internal static Func LoadFromFile { get; set; } = Assembly.LoadFrom; + + private static readonly ILoggerService s_logger = + LoggerServiceFactory.GetLogger(typeof(AssemblyLoader)); + + private static readonly Dictionary s_assemblyCache = + new Dictionary(); + + // Lazily evaluate the assembly search paths because it has a dependency on SparkFiles. + private static readonly Lazy s_searchPaths = + new Lazy(() => AssemblySearchPathResolver.GetAssemblySearchPaths()); + + private static readonly string[] s_extensions = + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + new[] { ".dll", ".exe", ".ni.dll", ".ni.exe" } : + new[] { ".dll", ".ni.dll" }; + + private static readonly object s_cacheLock = new object(); + + // Roslyn generates assembly names with characters that cause issues. + // The generated name contains *, a reserved character in Windows, + // and #, which causes problems when used with SparkContext.AddFile. + // https://github.com/dotnet/roslyn/blob/da63493c37e4a450076d6dac02044bf0fcdbcc50/src/Scripting/Core/ScriptBuilder.cs#L51 + private static readonly Regex s_roslynAssemblyNameRegex = + new Regex( + "^\u211B\\*([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})#([0-9]+-[0-9]+)", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + /// + /// Return the cached assembly, otherwise attempt to load and cache the assembly + /// by searching for the assembly filename in the search paths. + /// + /// The full name of the assembly + /// Name of the file that contains the assembly + /// Cached or Loaded Assembly or null if not found + internal static Assembly LoadAssembly(string assemblyName, string assemblyFileName) + { + // assemblyFileName is empty when serializing a UDF from within the REPL. + if (string.IsNullOrWhiteSpace(assemblyFileName)) + { + return ResolveAssembly(assemblyName); + } + + lock (s_cacheLock) + { + if (s_assemblyCache.TryGetValue(assemblyName, out Assembly assembly)) + { + return assembly; + } + + if (TryLoadAssembly(assemblyFileName, ref assembly)) + { + s_assemblyCache[assemblyName] = assembly; + return assembly; + } + + s_logger.LogWarn( + string.Format( + "Assembly '{0}' file not found '{1}' in '{2}'", + assemblyName, + assemblyFileName, + string.Join(",", s_searchPaths.Value))); + + return null; + } + } + + /// + /// Return the cached assembly, otherwise look in the probing paths returned + /// by AssemblySearchPathResolver, searching for the simple assembly name and + /// s_extension combination. + /// + /// The fullname of the assembly to load + /// The loaded assembly or null if not found + internal static Assembly ResolveAssembly(string assemblyName) + { + lock (s_cacheLock) + { + if (s_assemblyCache.TryGetValue(assemblyName, out Assembly assembly)) + { + return assembly; + } + + string simpleAsmName = + NormalizeAssemblyName(new AssemblyName(assemblyName).Name); + foreach (string extension in s_extensions) + { + string assemblyFileName = $"{simpleAsmName}{extension}"; + if (TryLoadAssembly(assemblyFileName, ref assembly)) + { + s_assemblyCache[assemblyName] = assembly; + return assembly; + } + } + + s_logger.LogWarn( + string.Format( + "Assembly '{0}' file not found '{1}[{2}]' in '{3}'", + assemblyName, + simpleAsmName, + string.Join(",", s_extensions), + string.Join(",", s_searchPaths.Value))); + + return null; + } + } + + /// + /// Returns the loaded assembly by probing paths returned by AssemblySearchPathResolver. + /// + /// Name of the file that contains the assembly + /// The loaded assembly. + /// True if assembly is loaded, false otherwise. + private static bool TryLoadAssembly(string assemblyFileName, ref Assembly assembly) + { + foreach (string searchPath in s_searchPaths.Value) + { + var assemblyFile = new FileInfo(Path.Combine(searchPath, assemblyFileName)); + if (assemblyFile.Exists) + { + try + { + assembly = LoadFromFile(assemblyFile.FullName); + return true; + } + catch (Exception ex) when ( + ex is FileLoadException || + ex is BadImageFormatException) + { + // Ignore invalid assemblies. + } + } + } + + return false; + } + + /// + /// Normalizes the assemblyName by removing characters known to cause + /// issues. This is useful in situations where the assemblyName is + /// automatically generated, ie the Roslyn compiler used in the REPL + /// generates an assembly name that contains * and #. + /// + /// Assembly name + /// Normalized assembly name + internal static string NormalizeAssemblyName(string assemblyName) + { + // Check if the assembly name follows the Roslyn naming convention. + // Roslyn assembly name: "\u211B*4b31b71b-d4bd-4642-9f63-eef5f5d99197#1-14" + // Normalized Roslyn assembly name: "4b31b71b-d4bd-4642-9f63-eef5f5d99197-1-14" + Match match = s_roslynAssemblyNameRegex.Match(assemblyName); + return match.Success ? + $"{match.Groups[1].Value}-{match.Groups[2].Value}" : + assemblyName; + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/Authenticator.cs b/src/spark/Flowthru.Spark/Utils/Authenticator.cs new file mode 100644 index 00000000..cc06e1a7 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/Authenticator.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Network; + +namespace Flowthru.Spark.Utils +{ + /// + /// Authenticator provides functionalities to authenticate between + /// Spark and .NET worker. + /// + internal static class Authenticator + { + private static readonly string s_validResponseCode = "ok"; + private static readonly string s_invalidResponseCode = "err"; + + /// + /// Authenticates by writing secret to stream and validate the response. + /// + /// Valid stream. + /// Secret string to authenticate against. + /// True if authentication succeeds. + public static bool AuthenticateAsClient(Stream stream, string secret) + { + SerDe.Write(stream, secret); + stream.Flush(); + + return SerDe.ReadString(stream) == s_validResponseCode; + } + + /// + /// Authenticates by reading secret from stream and writes the response code + /// back to the stream. + /// + /// Valid socket. + /// Secret string to authenticate against. + /// True if authentication succeeds. + public static bool AuthenticateAsServer(ISocketWrapper socket, string secret) + { + string clientSecret = SerDe.ReadString(socket.InputStream); + + bool result; + if (clientSecret == secret) + { + SerDe.Write(socket.OutputStream, s_validResponseCode); + result = true; + } + else + { + SerDe.Write(socket.OutputStream, s_invalidResponseCode); + result = false; + } + + socket.OutputStream.Flush(); + return result; + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/BinarySerDe.cs b/src/spark/Flowthru.Spark/Utils/BinarySerDe.cs new file mode 100644 index 00000000..0cc10093 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/BinarySerDe.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using MessagePack; +using MessagePack.Resolvers; +using Flowthru.Spark.Interop.Ipc; + +namespace Flowthru.Spark.Utils; + +// If deserialization of untrusted data is required, extend this functionality to +// incorporate techniques such as using a Message Authentication Code (MAC) +// or whitelisting allowed types to mitigate security risks. + +/// +/// BinarySerDe (Serialization/Deserialization) is a utility class designed to handle +/// serialization and deserialization of objects to and from binary formats. +/// +/// +/// This implementation uses the MessagePack `Typeless` API, which embeds type +/// information into the serialized data. It adds overhead of 1-2 bytes for primitive +/// types, and serializes 'System.Type' entirely for complex objects. +/// Does not serialize type definition, so in order to deserialize a complex object, +/// declaring library should be available in app domain or at probing locations. +/// +/// +internal static class BinarySerDe +{ + private static MessagePackSerializerOptions _options = + new AllowStandardOrSerializableMessagePackSerializerOptions( + TypelessContractlessStandardResolver.Instance + ).WithSecurity(MessagePackSecurity.UntrustedData); + + /// + /// Deserializes a stream of binary data into an object of type T. + /// When using or shared streams, prefer the overloaded version + /// that accepts a `length` parameter to ensure no excess data is consumed. + /// + /// The expected type of the deserialized object. + /// The stream containing the serialized data. + /// An object of type T. + internal static T Deserialize(Stream stream) + { + return (T)MessagePackSerializer.Typeless.Deserialize(stream, _options); + } + + /// + /// Deserializes an object from stream, ensuring no excess data is read. + /// + /// The stream containing the serialized data. + /// The length of byte section to deserialize. + /// The deserialized object. + internal static object Deserialize(Stream stream, int length) + { + ReadOnlyMemory memory = SerDe.ReadBytes(stream, length); + + return MessagePackSerializer.Typeless.Deserialize(memory, _options); + } + + /// + /// Serializes an object into a binary stream + /// + /// The type of the object to serialize. + /// The target stream where the data will be written. + /// The object to serialize. + internal static void Serialize(Stream stream, T graph) + { + MessagePackSerializer.Typeless.Serialize(stream, graph, _options); + } +} + +/// +/// Additional security for MessagePack typeless serialization, that only allows +/// standard classes or classes marked with the 'System.Serializable' attribute. +/// +internal class AllowStandardOrSerializableMessagePackSerializerOptions + : MessagePackSerializerOptions +{ + public AllowStandardOrSerializableMessagePackSerializerOptions(IFormatterResolver resolver) + : base(resolver) { } + + protected AllowStandardOrSerializableMessagePackSerializerOptions( + MessagePackSerializerOptions copyFrom + ) + : base(copyFrom) { } + + public override void ThrowIfDeserializingTypeIsDisallowed(Type type) + { + // Check against predefined blacklist + base.ThrowIfDeserializingTypeIsDisallowed(type); + + // Check if MessagePack can handle this type safely + var formatter = StandardResolver.Instance.GetFormatterDynamic(type); + + if ( + formatter == null + && type.GetCustomAttributes(typeof(System.SerializableAttribute), true).Length == 0 + ) + { + throw new MessagePackSerializationException( + $"Deserialization attempted to create the type {type.FullName} which is not allowed." + + $" Add 'System.Serializable' attribute to allow serialization" + ); + } + } + + protected override MessagePackSerializerOptions Clone() + { + return new AllowStandardOrSerializableMessagePackSerializerOptions(this); + } +} diff --git a/src/spark/Flowthru.Spark/Utils/CollectionUtils.cs b/src/spark/Flowthru.Spark/Utils/CollectionUtils.cs new file mode 100644 index 00000000..518d4cc1 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/CollectionUtils.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; + +namespace Flowthru.Spark.Utils +{ + internal static class CollectionUtils + { + internal static bool ArrayEquals(T[] array1, T[] array2) + { + return (array1?.Length == array2?.Length) && + ((array1 == null) || array1.SequenceEqual(array2)); + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/CommandSerDe.cs b/src/spark/Flowthru.Spark/Utils/CommandSerDe.cs new file mode 100644 index 00000000..64433cd1 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/CommandSerDe.cs @@ -0,0 +1,418 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.Utils +{ + /// + /// CommandSerDe provides functionality to serialize/deserialize WorkerFunction + /// along with other information. + /// + internal static class CommandSerDe + { + internal enum SerializedMode + { + None, + String, + Byte, + Pair, + Row + } + + /// + /// The function name of any UDF wrappers that wrap the UDF. + /// ex) + /// + private const string UdfWrapperMethodName = "Execute"; + + /// + /// Captures the information about the UDF wrapper. + /// Example classes for wrapping UDF are: + /// - SQL: * + /// * + /// - RDD: * + /// * + /// * + /// * + /// + [Serializable] + private sealed class UdfWrapperNode + { + /// + /// Type name of the UDF wrapper. + /// + internal string TypeName { get; set; } + + /// + /// Number of children (UDF wrapper or UDF) this node is associated with. + /// Note that there can be up to two children and if the child is an UDF, + /// this will be set to one. + /// + internal int NumChildren { get; set; } + + /// + /// True if the child is an UDF. + /// + internal bool HasUdf { get; set; } + } + + /// + /// UdfWrapperData represents the flattened tree structure. + /// For example: + /// WorkerChainHelper#1 + /// / \ + /// WorkerChainHelper#2 MapUdfWrapper#3 + /// / \ \ + /// MapUdfWrapper#1 MapUdfWrapper#2 UDF#3 + /// | | + /// UDF#1 UDF#2 + /// + /// will be translated into: + /// UdfWrapperNodes: (WorkerChainHelper(WCH), MapUdfWrapper(MUW)) + /// [ WCH#1(2, false), WCH#2(2, false), MUW#1(1, true), MUW#2(1, true), MUW#3(1, true) ] + /// where WCH#1(2, false) means the node has two children and HasUdf is false. + /// Udfs: + /// [ UDF#1, UDF#2, UDF#3 ] + /// + /// + [Serializable] + private sealed class UdfWrapperData + { + /// + /// Flattened UDF wrapper nodes. + /// + internal UdfWrapperNode[] UdfWrapperNodes { get; set; } + + /// + /// Serialized UDF data. + /// + internal UdfSerDe.UdfData[] Udfs { get; set; } + } + + internal static byte[] Serialize( + Delegate func, + SerializedMode deserializerMode = SerializedMode.Byte, + SerializedMode serializerMode = SerializedMode.Byte) + { + // TODO: Rework on the following List to use MemoryStream! + + var commandPayloadBytesList = new List(); + + // Add serializer mode. + byte[] modeBytes = Encoding.UTF8.GetBytes(serializerMode.ToString()); + int length = modeBytes.Length; + byte[] lengthAsBytes = BitConverter.GetBytes(length); + Array.Reverse(lengthAsBytes); + commandPayloadBytesList.Add(lengthAsBytes); + commandPayloadBytesList.Add(modeBytes); + + // Add deserializer mode. + modeBytes = Encoding.UTF8.GetBytes(deserializerMode.ToString()); + length = modeBytes.Length; + lengthAsBytes = BitConverter.GetBytes(length); + Array.Reverse(lengthAsBytes); + commandPayloadBytesList.Add(lengthAsBytes); + commandPayloadBytesList.Add(modeBytes); + + // Add run mode: + // N - normal + // R - repl + string runMode = Environment.GetEnvironmentVariable("SPARK_NET_RUN_MODE") ?? "N"; + byte[] runModeBytes = Encoding.UTF8.GetBytes(runMode); + lengthAsBytes = BitConverter.GetBytes(runModeBytes.Length); + Array.Reverse(lengthAsBytes); + commandPayloadBytesList.Add(lengthAsBytes); + commandPayloadBytesList.Add(runModeBytes); + + if ("R".Equals(runMode, StringComparison.InvariantCultureIgnoreCase)) + { + // add compilation dump directory + byte[] compilationDumpDirBytes = Encoding.UTF8.GetBytes( + Environment.GetEnvironmentVariable("SPARK_NET_SCRIPT_COMPILATION_DIR") ?? "."); + lengthAsBytes = BitConverter.GetBytes(compilationDumpDirBytes.Length); + Array.Reverse(lengthAsBytes); + commandPayloadBytesList.Add(lengthAsBytes); + commandPayloadBytesList.Add(compilationDumpDirBytes); + } + + // Serialize the UDFs. + var udfWrapperNodes = new List(); + var udfs = new List(); + SerializeUdfs(func, null, udfWrapperNodes, udfs); + + // Run through UdfSerDe.Serialize once more to get serialization info + // on the actual UDF. + var udfWrapperData = new UdfWrapperData() + { + UdfWrapperNodes = udfWrapperNodes.ToArray(), + Udfs = udfs.ToArray() + }; + + using (var stream = new MemoryStream()) + { + BinarySerDe.Serialize(stream, udfWrapperData); + + byte[] udfBytes = stream.ToArray(); + byte[] udfBytesLengthAsBytes = BitConverter.GetBytes(udfBytes.Length); + Array.Reverse(udfBytesLengthAsBytes); + commandPayloadBytesList.Add(udfBytesLengthAsBytes); + commandPayloadBytesList.Add(udfBytes); + } + + return commandPayloadBytesList.SelectMany(byteArray => byteArray).ToArray(); + } + + private static void SerializeUdfs( + Delegate func, + UdfWrapperNode parent, + List udfWrapperNodes, + List udfs) + { + UdfSerDe.UdfData udfData = UdfSerDe.Serialize(func); + if ((udfData.MethodName != UdfWrapperMethodName) || + !Attribute.IsDefined(func.Target.GetType(), typeof(UdfWrapperAttribute))) + { + // Found the actual UDF. + if (parent != null) + { + parent.HasUdf = true; + Debug.Assert(parent.NumChildren == 1); + } + + udfs.Add(udfData); + return; + } + + UdfSerDe.FieldData[] fields = udfData.TargetData.Fields; + if ((fields.Length == 0) || (fields.Length > 2)) + { + throw new Exception( + $"Invalid number of children ({fields.Length}) for {udfData.TypeData.Name}"); + } + + var curNode = new UdfWrapperNode + { + TypeName = udfData.TypeData.Name, + NumChildren = fields.Length, + HasUdf = false + }; + + udfWrapperNodes.Add(curNode); + + foreach (UdfSerDe.FieldData field in fields) + { + SerializeUdfs((Delegate)field.Value, curNode, udfWrapperNodes, udfs); + } + } + + internal static object DeserializeArrowOrDataFrameUdf( + Stream stream, + out SerializedMode serializerMode, + out SerializedMode deserializerMode, + out string runMode) + { + UdfWrapperData udfWrapperData = GetUdfWrapperDataFromStream( + stream, + out serializerMode, + out deserializerMode, + out runMode); + + int nodeIndex = 0; + int udfIndex = 0; + UdfWrapperNode node = udfWrapperData.UdfWrapperNodes[nodeIndex]; + Type nodeType = Type.GetType(node.TypeName); + Delegate udf; + if (nodeType == typeof(DataFrameGroupedMapUdfWrapper)) + { + udf = (DataFrameGroupedMapWorkerFunction.ExecuteDelegate)DeserializeUdfs( + udfWrapperData, + ref nodeIndex, + ref udfIndex); + } + else if (nodeType == typeof(DataFrameWorkerFunction) || nodeType.IsSubclassOf(typeof(DataFrameUdfWrapper))) + { + udf = (DataFrameWorkerFunction.ExecuteDelegate)DeserializeUdfs( + udfWrapperData, + ref nodeIndex, + ref udfIndex); + } + else if (nodeType == typeof(ArrowGroupedMapUdfWrapper)) + { + udf = (ArrowGroupedMapWorkerFunction.ExecuteDelegate)DeserializeUdfs( + udfWrapperData, + ref nodeIndex, + ref udfIndex); + } + else + { + udf = (ArrowWorkerFunction.ExecuteDelegate) + DeserializeUdfs( + udfWrapperData, + ref nodeIndex, + ref udfIndex); + } + + // Check all the data is consumed. + Debug.Assert(nodeIndex == udfWrapperData.UdfWrapperNodes.Length); + Debug.Assert(udfIndex == udfWrapperData.Udfs.Length); + + return udf; + } + + private static UdfWrapperData GetUdfWrapperDataFromStream( + Stream stream, + out SerializedMode serializerMode, + out SerializedMode deserializerMode, + out string runMode) + { + if (!Enum.TryParse(SerDe.ReadString(stream), out serializerMode)) + { + throw new InvalidDataException("Serializer mode is not valid."); + } + + if (!Enum.TryParse(SerDe.ReadString(stream), out deserializerMode)) + { + throw new InvalidDataException("Deserializer mode is not valid."); + } + + runMode = SerDe.ReadString(stream); + + byte[] serializedCommand = SerDe.ReadBytes(stream); + + var ms = new MemoryStream(serializedCommand, false); + + return BinarySerDe.Deserialize(ms); + } + + internal static T Deserialize( + Stream stream, + out SerializedMode serializerMode, + out SerializedMode deserializerMode, + out string runMode) where T : Delegate + { + UdfWrapperData udfWrapperData = GetUdfWrapperDataFromStream( + stream, + out serializerMode, + out deserializerMode, + out runMode); + int nodeIndex = 0; + int udfIndex = 0; + T udf = (T)DeserializeUdfs(udfWrapperData, ref nodeIndex, ref udfIndex); + + // Check all the data is consumed. + Debug.Assert(nodeIndex == udfWrapperData.UdfWrapperNodes.Length); + Debug.Assert(udfIndex == udfWrapperData.Udfs.Length); + + return udf; + } + + /// + /// Deserializes a non-UDF command from the stream. + /// This method handles both RDD commands and Raw commands, detecting the type + /// from the serialized wrapper information. + /// + /// Raw UDFs provide direct access to input/output streams for high-performance + /// scenarios where standard row-by-row processing is not efficient enough. + /// + /// Stream to read from + /// Output serialization mode + /// Output deserialization mode + /// Output run mode + /// Either RDD.WorkerFunction.ExecuteDelegate or RawWorkerFunction.ExecuteDelegate + internal static object DeserializeNonUdf( + Stream stream, + out SerializedMode serializerMode, + out SerializedMode deserializerMode, + out string runMode) + { + UdfWrapperData udfWrapperData = GetUdfWrapperDataFromStream( + stream, + out serializerMode, + out deserializerMode, + out runMode); + int nodeIndex = 0; + int udfIndex = 0; + UdfWrapperNode node = udfWrapperData.UdfWrapperNodes[nodeIndex]; + Type nodeType = Type.GetType(node.TypeName); + Delegate udf; + if (nodeType == typeof(RawUdfWrapper)) + { + udf = DeserializeUdfs( + udfWrapperData, + ref nodeIndex, + ref udfIndex); + } + else + { + udf = DeserializeUdfs( + udfWrapperData, + ref nodeIndex, + ref udfIndex); + } + + // Check all the data is consumed. + Debug.Assert(nodeIndex == udfWrapperData.UdfWrapperNodes.Length); + Debug.Assert(udfIndex == udfWrapperData.Udfs.Length); + + return udf; + } + + private static Delegate DeserializeUdfs( + UdfWrapperData data, + ref int nodeIndex, + ref int udfIndex) + { + UdfWrapperNode node = data.UdfWrapperNodes[nodeIndex++]; + Type nodeType = Type.GetType(node.TypeName); + + if (node.HasUdf) + { + var udfs = new object[node.NumChildren]; + for (int i = 0; i < node.NumChildren; ++i) + { + udfs[i] = UdfSerDe.Deserialize(data.Udfs[udfIndex++]); + } + + return CreateUdfWrapperDelegate(nodeType, udfs); + } + + var udfWrappers = new object[node.NumChildren]; + for (int i = 0; i < node.NumChildren; ++i) + { + udfWrappers[i] = DeserializeUdfs(data, ref nodeIndex, ref udfIndex); + } + + return CreateUdfWrapperDelegate(nodeType, udfWrappers); + } + + private static Delegate CreateUdfWrapperDelegate(Type type, object[] parameters) + { + BindingFlags bindingFlags = BindingFlags.Instance | + BindingFlags.Static | + BindingFlags.NonPublic | + BindingFlags.Public; + + object udfWrapper = Activator.CreateInstance( + type, + bindingFlags, + null, + parameters, + null); + + return Delegate.CreateDelegate( + typeof(T), + udfWrapper, + UdfWrapperMethodName); + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/DataFrameUdfUtils.cs b/src/spark/Flowthru.Spark/Utils/DataFrameUdfUtils.cs new file mode 100644 index 00000000..c2145816 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/DataFrameUdfUtils.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Apache.Arrow; +using Microsoft.Data.Analysis; +using Flowthru.Spark.Sql; + +namespace Flowthru.Spark.Utils +{ + using DataFrameDelegate = DataFrameWorkerFunction.ExecuteDelegate; + + /// + /// DataFrameUdfUtils provides utility functions to wrap UDFs that use Microsoft.Data.Analysis + /// + internal static class DataFrameUdfUtils + { + internal static Delegate CreateVectorUdfWrapper(Func udf) + where T : DataFrameColumn + where TResult : DataFrameColumn + { + return (DataFrameDelegate)new DataFrameUdfWrapper(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper(Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where TResult : DataFrameColumn + { + return (DataFrameDelegate)new DataFrameUdfWrapper(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where TResult : DataFrameColumn + { + return (DataFrameDelegate)new DataFrameUdfWrapper(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where TResult : DataFrameColumn + { + return (DataFrameDelegate) + new DataFrameUdfWrapper(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where TResult : DataFrameColumn + { + return (DataFrameDelegate) + new DataFrameUdfWrapper(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where TResult : DataFrameColumn + { + return (DataFrameDelegate) + new DataFrameUdfWrapper< + T1, T2, T3, T4, T5, T6, TResult>(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where TResult : DataFrameColumn + { + return (DataFrameDelegate) + new DataFrameUdfWrapper< + T1, T2, T3, T4, T5, T6, T7, TResult>(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where TResult : DataFrameColumn + { + return (DataFrameDelegate) + new DataFrameUdfWrapper< + T1, T2, T3, T4, T5, T6, T7, T8, TResult>(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where T9 : DataFrameColumn + where TResult : DataFrameColumn + { + return (DataFrameDelegate) + new DataFrameUdfWrapper< + T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult>(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : DataFrameColumn + where T2 : DataFrameColumn + where T3 : DataFrameColumn + where T4 : DataFrameColumn + where T5 : DataFrameColumn + where T6 : DataFrameColumn + where T7 : DataFrameColumn + where T8 : DataFrameColumn + where T9 : DataFrameColumn + where T10 : DataFrameColumn + where TResult : DataFrameColumn + { + return (DataFrameDelegate) + new DataFrameUdfWrapper< + T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult>(udf).Execute; + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/DependencyProviderUtils.cs b/src/spark/Flowthru.Spark/Utils/DependencyProviderUtils.cs new file mode 100644 index 00000000..b786e963 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/DependencyProviderUtils.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; + +namespace Flowthru.Spark.Utils +{ + internal class DependencyProviderUtils + { + private static readonly string s_filePattern = "dependencyProviderMetadata_*"; + + internal static string[] GetMetadataFiles(string path) => + Directory.GetFiles(path, s_filePattern); + + // Create the dependency provider metadata filename based on the number and + // the first 8 characters of guid passed into the function. + // + // number => filename + // 0 => dependencyProviderMetadata_f1a2b3c400000000000 + // 1 => dependencyProviderMetadata_f1a2b3c400000000001 + // ... + // 20 => dependencyProviderMetadata_f1a2b3c400000000020 + internal static string CreateFileName(Guid runId, long number) => + s_filePattern.Replace("*", $"{runId.ToString("N").Substring(0, 8)}{number:D11}"); + + [Serializable] + internal class NuGetMetadata + { + public string FileName { get; set; } + public string PackageName { get; set; } + public string PackageVersion { get; set; } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override bool Equals(object obj) + { + return (obj is NuGetMetadata nugetMetadata) && + Equals(nugetMetadata); + } + + private bool Equals(NuGetMetadata other) + { + return (other != null) && + (FileName == other.FileName) && + (PackageName == other.PackageName) && + (PackageVersion == other.PackageVersion); + } + } + + [Serializable] + internal class Metadata + { + public string[] AssemblyProbingPaths { get; set; } + public string[] NativeProbingPaths { get; set; } + public NuGetMetadata[] NuGets { get; set; } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override bool Equals(object obj) + { + return (obj is Metadata metadata) && + Equals(metadata); + } + + internal static Metadata Deserialize(string path) + { + using FileStream fileStream = File.OpenRead(path); + return BinarySerDe.Deserialize(fileStream); + } + + internal void Serialize(string path) + { + using FileStream fileStream = File.OpenWrite(path); + BinarySerDe.Serialize(fileStream, this); + } + + private bool Equals(Metadata other) + { + return (other != null) && + CollectionUtils.ArrayEquals( + AssemblyProbingPaths, + other.AssemblyProbingPaths) && + CollectionUtils.ArrayEquals(NativeProbingPaths, other.NativeProbingPaths) && + CollectionUtils.ArrayEquals(NuGets, other.NuGets); + } + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/EnvironmentUtils.cs b/src/spark/Flowthru.Spark/Utils/EnvironmentUtils.cs new file mode 100644 index 00000000..f410b14b --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/EnvironmentUtils.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Flowthru.Spark.Utils +{ + /// + /// Various environment utility methods. + /// + internal static class EnvironmentUtils + { + internal static bool GetEnvironmentVariableAsBool(string name) + { + string str = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(str)) + { + return false; + } + + switch (str.ToLowerInvariant()) + { + case "true": + case "1": + case "yes": + return true; + case "false": + case "0": + case "no": + return false; + default: + return false; + } + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/JvmObjectUtils.cs b/src/spark/Flowthru.Spark/Utils/JvmObjectUtils.cs new file mode 100644 index 00000000..6aa83125 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/JvmObjectUtils.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Reflection; +using Flowthru.Spark.Interop.Ipc; +using System.Collections.Generic; + +namespace Flowthru.Spark.Utils +{ + /// + /// Provides general helper functions related to JVM objects. + /// + internal class JvmObjectUtils + { + /// + /// Search through all assemblies in current domain and find those types + /// that are subclasses of the parentType. For those types we find its + /// javaClassFieldName value, which is its java-side consistent name, then + /// construct a mapping between the java class name and dotnet class type. + /// Please note that types containing generic parameters are not supported. + /// + /// The parent class of the target type. + /// The private static string field name of the dotnet class. + /// A mapping of java class name and dotnet class type. + internal static Dictionary ConstructJavaClassMapping( + Type parentType, + string javaClassFieldName) + { + // a mapping of java class name to the dotnet type + var classMapping = new Dictionary(); + // search within the assemblies to find the real type that matches returnClass name + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + foreach (Type type in assembly.GetTypes().Where(type => + type.IsClass && + !type.IsAbstract && + type.IsSubclassOf(parentType) && + !type.ContainsGenericParameters)) + { + FieldInfo info = type.GetField(javaClassFieldName, BindingFlags.NonPublic | BindingFlags.Static); + var classNameValue = (string)info.GetValue(null); + if (!string.IsNullOrWhiteSpace(classNameValue)) + { + classMapping.Add(classNameValue, type); + } + } + } + + return classMapping; + } + + /// + /// Get java class name from the jvm object; search through the mapping + /// between java class name and dotnet class type for the current domain; + /// construct dotnet class instance by calling the constructor with jvmObject + /// as the parameter, and cast it to type T. If the java class name doesn't + /// exist in the mapping we assign the instance its default value. + /// + /// The reference to object created in JVM. + /// The mapping between java class name and dotnet class type. + /// The constructed dotnet object instance. + /// The casting type of dotnet object instance. + /// Whether we successfully constructed the dotnet object instance or not. + internal static bool TryConstructInstanceFromJvmObject( + JvmObjectReference jvmObject, + Dictionary classMapping, + out T instance) + { + var jvmClass = (JvmObjectReference)jvmObject.Invoke("getClass"); + var returnClass = (string)jvmClass.Invoke("getTypeName"); + if (classMapping.ContainsKey(returnClass)) + { + Type dotnetType = classMapping[returnClass]; + instance = (T)dotnetType.Assembly.CreateInstance( + dotnetType.FullName, + false, + BindingFlags.Instance | BindingFlags.NonPublic, + null, + new object[] { jvmObject }, + null, + null); + + return true; + } + + instance = default; + return false; + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/PythonSerDe.cs b/src/spark/Flowthru.Spark/Utils/PythonSerDe.cs new file mode 100644 index 00000000..5adabeaf --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/PythonSerDe.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.IO; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; +using Razorvine.Pickle; +using Razorvine.Pickle.Objects; + +namespace Flowthru.Spark.Utils +{ + /// + /// Used for SerDe of Python objects. + /// + internal class PythonSerDe + { + // One RowConstructor object is registered to the Unpickler and + // there could be multiple threads unpickling row data using + // this object. However there is no issue as the field(s) that are + // reused by this object are instantiated on a per-thread basis and + // therefore not shared between threads. + private static readonly RowConstructor s_rowConstructor; + + static PythonSerDe() + { + // Custom picklers used in PySpark implementation. + // Refer to spark/python/pyspark/sql/types.py. + Unpickler.registerConstructor( + "pyspark.sql.types", "_parse_datatype_json_string", new StringConstructor()); + + s_rowConstructor = new RowConstructor(); + Unpickler.registerConstructor( + "pyspark.sql.types", "_create_row_inbound_converter", s_rowConstructor); + + // Register custom picklers. + Pickler.registerCustomPickler(typeof(Row), new RowPickler()); + Pickler.registerCustomPickler(typeof(GenericRow), new GenericRowPickler()); + Pickler.registerCustomPickler(typeof(Date), new DatePickler()); + Pickler.registerCustomPickler(typeof(Timestamp), new TimestampPickler()); + } + + /// + /// Unpickles objects from Stream. + /// + /// Pickled byte stream + /// Size (in bytes) of the pickled input + /// Unpicked objects + internal static object[] GetUnpickledObjects(Stream stream, int messageLength) + { + byte[] buffer = ArrayPool.Shared.Rent(messageLength); + + try + { + if (!SerDe.TryReadBytes(stream, buffer, messageLength)) + { + throw new ArgumentException("The stream is closed."); + } + + var unpickler = new Unpickler(); + object unpickledItems = unpickler.loads( + new ReadOnlyMemory(buffer, 0, messageLength), + stackCapacity: 102); // Spark sends batches of 100 rows, and +2 is for markers. + s_rowConstructor.Reset(); + Debug.Assert(unpickledItems != null); + return (unpickledItems as object[]); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/RawUdfWrapper.cs b/src/spark/Flowthru.Spark/Utils/RawUdfWrapper.cs new file mode 100644 index 00000000..cc8c1967 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/RawUdfWrapper.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using static Flowthru.Spark.Utils.CommandSerDe; + +namespace Flowthru.Spark.Utils +{ + /// + /// RawUdfWrapper wraps a raw UDF function that operates directly on streams. + /// This wrapper is used for serialization/deserialization of raw UDFs that need + /// direct access to input/output streams for high-performance data processing. + /// + /// Unlike standard UDFs that process data row-by-row, raw UDFs receive the entire + /// input stream and have full control over how data is read and written. + /// + [UdfWrapper] + internal sealed class RawUdfWrapper + { + private readonly Func _func; + + internal RawUdfWrapper(Func func) + { + _func = func; + } + + internal int Execute( + int pid, + Stream inputStream, + Stream outputStream, + SerializedMode serializedMode, + SerializedMode deserializedMode) + { + return _func(pid, inputStream, outputStream, serializedMode, deserializedMode); + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/RawWorkerFunction.cs b/src/spark/Flowthru.Spark/Utils/RawWorkerFunction.cs new file mode 100644 index 00000000..3f7f0650 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/RawWorkerFunction.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using static Flowthru.Spark.Utils.CommandSerDe; + +namespace Flowthru.Spark.Utils +{ + /// + /// RawWorkerFunction provides direct access to input/output streams for UDF execution. + /// This enables high-performance scenarios where the user needs direct control over + /// serialization/deserialization, bypassing the standard row-by-row processing. + /// + /// Use cases: + /// - Custom binary protocols for efficient data transfer + /// - Streaming large data without intermediate object allocation + /// - Integration with external serialization libraries + /// + internal sealed class RawWorkerFunction + { + /// + /// Delegate for raw UDF execution with direct stream access. + /// + /// The partition/split index being processed + /// Raw input stream from Spark + /// Raw output stream to Spark + /// Mode for serializing output data + /// Mode for deserializing input data + /// Number of entries processed + internal delegate int ExecuteDelegate( + int splitId, + Stream inputStream, + Stream outputStream, + SerializedMode serializedMode, + SerializedMode deserializedMode); + + public RawWorkerFunction(ExecuteDelegate func) + { + Func = func; + } + + internal ExecuteDelegate Func { get; } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/TypeConverter.cs b/src/spark/Flowthru.Spark/Utils/TypeConverter.cs new file mode 100644 index 00000000..2b295a33 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/TypeConverter.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Flowthru.Spark.Utils +{ + /// + /// Supports converting to a typed and + /// to a typed . + /// + internal static class TypeConverter + { + /// + /// Convert obj to type . + /// + /// Type to convert to + /// The object to convert + /// Converted object. + internal static T ConvertTo(object obj) => (T)Convert(obj, typeof(T)); + + private static object Convert(object obj, Type toType) + { + if ((obj is ArrayList arrayList) && toType.IsArray) + { + return ConvertToArray(arrayList, toType); + } + else if ((obj is Hashtable hashtable) && toType.IsGenericType && + (toType.GetGenericTypeDefinition() == typeof(Dictionary<,>))) + { + return ConvertToDictionary(hashtable, toType); + } + // Fails to convert int to long otherwise + else if (toType.IsPrimitive) + { + return System.Convert.ChangeType(obj, toType); + } + + return obj; + } + + private static object ConvertToArray(ArrayList arrayList, Type type) + { + Type elementType = type.GetElementType(); + int length = arrayList.Count; + Array convertedArray = Array.CreateInstance(elementType, length); + for (int i = 0; i < length; ++i) + { + convertedArray.SetValue(Convert(arrayList[i], elementType), i); + } + + return convertedArray; + } + + private static object ConvertToDictionary(Hashtable hashtable, Type type) + { + Type[] genericTypes = type.GetGenericArguments(); + var dict = + (IDictionary)Activator.CreateInstance(type, new object[] { hashtable.Count }); + foreach (DictionaryEntry entry in hashtable) + { + dict[Convert(entry.Key, genericTypes[0])] = Convert(entry.Value, genericTypes[1]); + } + + return dict; + } + } +} diff --git a/src/spark/Flowthru.Spark/Utils/UdfSerDe.cs b/src/spark/Flowthru.Spark/Utils/UdfSerDe.cs new file mode 100644 index 00000000..f8df51e1 --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/UdfSerDe.cs @@ -0,0 +1,280 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.Serialization; + +namespace Flowthru.Spark.Utils +{ + /// + /// UdfSerDe is responsible for serializing/deserializing an UDF. + /// + internal class UdfSerDe + { + private static readonly ConcurrentDictionary s_typeCache = + new ConcurrentDictionary(); + + [Serializable] + internal sealed class TypeData : IEquatable + { + public string Name { get; set; } + public string AssemblyName { get; set; } + public string AssemblyFileName { get; set; } + + public override int GetHashCode() + { + // Simply hash on the Type's Name, which should provide + // a "unique enough" hash code. + return Name.GetHashCode(); + } + + public override bool Equals(object obj) + { + return (obj is TypeData typeData) && + Equals(typeData); + } + + public bool Equals(TypeData other) + { + return (other != null) && + (other.Name == Name) && + (other.AssemblyName == AssemblyName) && + (other.AssemblyFileName == AssemblyFileName); + } + } + + [Serializable] + internal sealed class UdfData + { + public TypeData TypeData { get; set; } + public string MethodName { get; set; } + public TargetData TargetData { get; set; } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override bool Equals(object obj) + { + return (obj is UdfData udfData) && + Equals(udfData); + } + + public bool Equals(UdfData other) + { + return (other != null) && + TypeData.Equals(other.TypeData) && + (MethodName == other.MethodName) && + TargetData.Equals(other.TargetData); + } + } + + [Serializable] + internal sealed class TargetData + { + public TypeData TypeData { get; set; } + public FieldData[] Fields { get; set; } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override bool Equals(object obj) + { + return (obj is TargetData targetData) && + Equals(targetData); + } + + public bool Equals(TargetData other) + { + if ((other == null) || + !TypeData.Equals(other.TypeData) || + (Fields?.Length != other.Fields?.Length)) + { + return false; + } + + if ((Fields == null) && (other.Fields == null)) + { + return true; + } + + return Fields.SequenceEqual(other.Fields); + } + } + + [Serializable] + internal sealed class FieldData + { + public TypeData TypeData { get; set; } + public string Name { get; set; } + public object Value { get; set; } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override bool Equals(object obj) + { + return (obj is FieldData fieldData) && + Equals(fieldData); + } + + public bool Equals(FieldData other) + { + return (other != null) && + TypeData.Equals(other.TypeData) && + (Name == other.Name) && + (((Value == null) && (other.Value == null)) || + ((Value != null) && Value.Equals(other.Value))); + } + } + + internal static UdfData Serialize(Delegate udf) + { + MethodInfo method = udf.Method; + object target = udf.Target; + + var udfData = new UdfData() + { + TypeData = SerializeType(method.DeclaringType), + MethodName = method.Name, + TargetData = SerializeTarget(target) + }; + + return udfData; + } + + internal static Delegate Deserialize(UdfData udfData) + { + Type udfType = DeserializeType(udfData.TypeData); + MethodInfo udfMethod = udfType.GetMethod( + udfData.MethodName, + BindingFlags.Instance | + BindingFlags.Static | + BindingFlags.Public | + BindingFlags.NonPublic); + + var udfParameters = udfMethod.GetParameters().Select(p => p.ParameterType).ToList(); + udfParameters.Add(udfMethod.ReturnType); + Type funcType = Expression.GetFuncType(udfParameters.ToArray()); + + if (udfData.TargetData == null) + { + // The given UDF is a static function. + return Delegate.CreateDelegate(funcType, udfMethod); + } + else + { + return Delegate.CreateDelegate( + funcType, + DeserializeTargetData(udfData.TargetData), + udfData.MethodName); + } + } + + private static TargetData SerializeTarget(object target) + { + // target will be null for static functions. + if (target == null) + { + return null; + } + + Type targetType = target.GetType(); + TypeData targetTypeData = SerializeType(targetType); + + var fields = new List(); + foreach (FieldInfo field in targetType.GetFields( + BindingFlags.Instance | + BindingFlags.Static | + BindingFlags.Public | + BindingFlags.NonPublic)) + { + if (!field.GetCustomAttributes(typeof(NonSerializedAttribute)).Any()) + { + fields.Add(new FieldData() + { + TypeData = SerializeType(field.FieldType), + Name = field.Name, + Value = field.GetValue(target) + }); + } + } + + // Even when an UDF does not have any closure, GetFields() returns some fields + // which include Func<> of the udf specified. + // For now, one way to distinguish is to check if any of the field's type + // is same as the target type. If so, fields will be emptied out. + // TODO: Follow up with the dotnet team. + bool doesUdfHaveClosure = fields. + Where((field) => field.TypeData.Name.Equals(targetTypeData.Name)). + Count() == 0; + + var targetData = new TargetData() + { + TypeData = targetTypeData, + Fields = doesUdfHaveClosure ? fields.ToArray() : null + }; + + return targetData; + } + + private static object DeserializeTargetData(TargetData targetData) + { + Type targetType = DeserializeType(targetData.TypeData); + object target = FormatterServices.GetUninitializedObject(targetType); + + foreach (FieldData field in targetData.Fields ?? Enumerable.Empty()) + { + targetType.GetField( + field.Name, + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic).SetValue(target, field.Value); + } + + return target; + } + + private static TypeData SerializeType(Type type) + { + return new TypeData() + { + Name = type.FullName, + AssemblyName = type.Assembly.FullName, + AssemblyFileName = Path.GetFileName(type.Assembly.Location) + }; + } + + private static Type DeserializeType(TypeData typeData) => + s_typeCache.GetOrAdd( + typeData, + td => + { + Type type = AssemblyLoader.LoadAssembly( + td.AssemblyName, + td.AssemblyFileName)?.GetType(td.Name, true); + if (type == null) + { + throw new FileNotFoundException( + string.Format( + "Assembly '{0}' file not found '{1}'", + td.AssemblyName, + td.AssemblyFileName)); + } + + return type; + }); + } +} diff --git a/src/spark/Flowthru.Spark/Utils/UdfUtils.cs b/src/spark/Flowthru.Spark/Utils/UdfUtils.cs new file mode 100644 index 00000000..4862ba3f --- /dev/null +++ b/src/spark/Flowthru.Spark/Utils/UdfUtils.cs @@ -0,0 +1,419 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Apache.Arrow; +using Microsoft.Data.Analysis; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Internal.Java.Util; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Spark.Utils +{ + using ArrowDelegate = ArrowWorkerFunction.ExecuteDelegate; + using PicklingDelegate = PicklingWorkerFunction.ExecuteDelegate; + + /// + /// UdfTypeUtils provides functions related to UDF types. + /// + internal static class UdfTypeUtils + { + /// + /// Returns "true" if the given type is nullable. + /// + /// Type to check if it is nullable + /// "true" if the given type is nullable. Otherwise, returns "false" + internal static string CanBeNull(this Type type) + { + return (!type.IsValueType || (Nullable.GetUnderlyingType(type) != null)) ? + "true" : + "false"; + } + + /// + /// Returns the generic type definition of a given type if the given type is equal + /// to or implements the `compare` type. Returns null if there is no match. + /// + /// This type object + /// Generic type definition to compare to + /// Matching generic type object + internal static Type ImplementsGenericTypeOf(this Type type, Type compare) + { + Debug.Assert(compare.IsGenericType); + return (type.IsGenericType && (type.GetGenericTypeDefinition() == compare)) ? + type : + type.GetInterface(compare.FullName); + } + } + + /// + /// UdfUtils provides UDF-related functions and enum. + /// + internal static class UdfUtils + { + /// + /// Enum for Python evaluation type. This determines how the data will be serialized + /// from Spark executor to its worker. + /// Since UDF is based on PySpark implementation, PythonEvalType is used. Once + /// generic interop layer is introduced, this will be revisited. + /// This mirrors values defined in python/pyspark/rdd.py. + /// + internal enum PythonEvalType + { + NON_UDF = 0, + + SQL_BATCHED_UDF = 100, + + SQL_SCALAR_PANDAS_UDF = 200, + SQL_GROUPED_MAP_PANDAS_UDF = 201, + SQL_GROUPED_AGG_PANDAS_UDF = 202, + SQL_WINDOW_AGG_PANDAS_UDF = 203, + + SQL_SCALAR_PANDAS_ITER_UDF = 204, + SQL_MAP_PANDAS_ITER_UDF = 205, + SQL_COGROUPED_MAP_PANDAS_UDF = 206 + } + + /// + /// Mapping of supported types from .NET to org.apache.spark.sql.types.DataType in Scala. + /// Refer to spark/sql/catalyst/src/main/scala/org/apache/spark/sql/types/DataType.scala + /// for more information. + /// + private static readonly Dictionary s_returnTypes = + new Dictionary + { + {typeof(string), "string"}, + {typeof(byte[]), "binary"}, + {typeof(bool), "boolean"}, + {typeof(decimal), "decimal(28,12)"}, + {typeof(double), "double"}, + {typeof(float), "float"}, + {typeof(byte), "byte"}, + {typeof(int), "integer"}, + {typeof(long), "long"}, + {typeof(short), "short"}, + {typeof(Date), "date"}, + {typeof(Timestamp), "timestamp"}, + + // Arrow array types + {typeof(BooleanArray), "boolean"}, + {typeof(UInt8Array), "byte"}, + {typeof(Int16Array), "short"}, + {typeof(Int32Array), "integer"}, + {typeof(Int64Array), "long"}, + {typeof(FloatArray), "float"}, + {typeof(DoubleArray), "double"}, + {typeof(StringArray), "string"}, + {typeof(BinaryArray), "binary"}, + + {typeof(BooleanDataFrameColumn), "boolean"}, + {typeof(ByteDataFrameColumn), "byte"}, + {typeof(Int16DataFrameColumn), "short"}, + {typeof(Int32DataFrameColumn), "integer"}, + {typeof(Int64DataFrameColumn), "long"}, + {typeof(SingleDataFrameColumn), "float"}, + {typeof(DoubleDataFrameColumn), "double"}, + {typeof(ArrowStringDataFrameColumn), "string"}, + }; + + /// + /// Returns the return type of an UDF in JSON format. This value is used to + /// create a org.apache.spark.sql.types.DataType object from JSON string. + /// + /// Return type of an UDF + /// JSON format of the return type + internal static string GetReturnType(Type type) + { + if (s_returnTypes.TryGetValue(type, out string value)) + { + return $@"""{value}"""; + } + + Type dictionaryType = type.ImplementsGenericTypeOf(typeof(IDictionary<,>)); + if (dictionaryType != null) + { + Type[] typeArguments = dictionaryType.GenericTypeArguments; + Type keyType = typeArguments[0]; + Type valueType = typeArguments[1]; + return @"{""type"":""map"", " + + $@"""keyType"":{GetReturnType(keyType)}, " + + $@"""valueType"":{GetReturnType(valueType)}, " + + $@"""valueContainsNull"":{valueType.CanBeNull()}}}"; + } + + Type enumerableType = type.ImplementsGenericTypeOf(typeof(IEnumerable<>)); + if (enumerableType != null) + { + Type elementType = enumerableType.GenericTypeArguments[0]; + return @"{""type"":""array"", " + + $@"""elementType"":{GetReturnType(elementType)}, " + + $@"""containsNull"":{elementType.CanBeNull()}}}"; + } + + throw new ArgumentException($"{type.FullName} is not supported."); + } + + /// + /// Creates the PythonFunction object on the JVM side wrapping the given command bytes. + /// + /// JVM bridge to use + /// Serialized command bytes + /// JvmObjectReference object to the PythonFunction object + internal static JvmObjectReference CreatePythonFunction(IJvmBridge jvm, byte[] command) + { + var arrayList = new ArrayList(jvm); + var broadcastVariables = new ArrayList(jvm); + broadcastVariables.AddAll(JvmBroadcastRegistry.GetAll()); + JvmBroadcastRegistry.Clear(); + + return (JvmObjectReference)jvm.CallStaticJavaMethod( + "org.apache.spark.sql.api.dotnet.SQLUtils", + "createPythonFunction", + command, + CreateEnvVarsForPythonFunction(jvm), + arrayList, // Python includes + SparkEnvironment.ConfigurationService.GetWorkerExePath(), + // Used to check the compatibility of UDFs between the driver and worker. + AssemblyInfoProvider.MicrosoftSparkAssemblyInfo().AssemblyVersion, + broadcastVariables, + null); // Accumulator + } + + private static IJvmObjectReferenceProvider CreateEnvVarsForPythonFunction(IJvmBridge jvm) + { + var environmentVars = new Hashtable(jvm); + string assemblySearchPath = Environment.GetEnvironmentVariable( + AssemblySearchPathResolver.AssemblySearchPathsEnvVarName); + if (!string.IsNullOrEmpty(assemblySearchPath)) + { + environmentVars.Put( + AssemblySearchPathResolver.AssemblySearchPathsEnvVarName, + assemblySearchPath); + } + // DOTNET_WORKER_SPARK_VERSION is used to handle different versions + // of Spark on the worker. + environmentVars.Put( + "DOTNET_WORKER_SPARK_VERSION", + SparkEnvironment.SparkVersion.ToString()); + + if (SparkEnvironment.ConfigurationService.IsRunningRepl()) + { + environmentVars.Put(Constants.RunningREPLEnvVar, "true"); + } + + return environmentVars; + } + + internal static Delegate CreateUdfWrapper(Func udf) + { + return (PicklingDelegate)new PicklingUdfWrapper(udf).Execute; + } + + internal static Delegate CreateUdfWrapper(Func udf) + { + return (PicklingDelegate)new PicklingUdfWrapper(udf).Execute; + } + + internal static Delegate CreateUdfWrapper(Func udf) + { + return (PicklingDelegate)new PicklingUdfWrapper(udf).Execute; + } + + internal static Delegate CreateUdfWrapper( + Func udf) + { + return (PicklingDelegate)new PicklingUdfWrapper(udf).Execute; + } + + internal static Delegate CreateUdfWrapper( + Func udf) + { + return (PicklingDelegate) + new PicklingUdfWrapper(udf).Execute; + } + + internal static Delegate CreateUdfWrapper( + Func udf) + { + return (PicklingDelegate) + new PicklingUdfWrapper(udf).Execute; + } + + internal static Delegate CreateUdfWrapper( + Func udf) + { + return (PicklingDelegate) + new PicklingUdfWrapper(udf).Execute; + } + + internal static Delegate CreateUdfWrapper( + Func udf) + { + return (PicklingDelegate) + new PicklingUdfWrapper(udf).Execute; + } + + internal static Delegate CreateUdfWrapper( + Func udf) + { + return (PicklingDelegate) + new PicklingUdfWrapper(udf).Execute; + } + + internal static Delegate CreateUdfWrapper( + Func udf) + { + return (PicklingDelegate) + new PicklingUdfWrapper< + T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult>(udf).Execute; + } + + internal static Delegate CreateUdfWrapper( + Func udf) + { + return (PicklingDelegate) + new PicklingUdfWrapper< + T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult>(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper(Func udf) + where T : IArrowArray + where TResult : IArrowArray + { + return (ArrowDelegate)new ArrowUdfWrapper(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper(Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where TResult : IArrowArray + { + return (ArrowDelegate)new ArrowUdfWrapper(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where TResult : IArrowArray + { + return (ArrowDelegate)new ArrowUdfWrapper(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where TResult : IArrowArray + { + return (ArrowDelegate) + new ArrowUdfWrapper(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where TResult : IArrowArray + { + return (ArrowDelegate) + new ArrowUdfWrapper(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where TResult : IArrowArray + { + return (ArrowDelegate) + new ArrowUdfWrapper< + T1, T2, T3, T4, T5, T6, TResult>(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where TResult : IArrowArray + { + return (ArrowDelegate) + new ArrowUdfWrapper< + T1, T2, T3, T4, T5, T6, T7, TResult>(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where TResult : IArrowArray + { + return (ArrowDelegate) + new ArrowUdfWrapper< + T1, T2, T3, T4, T5, T6, T7, T8, TResult>(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where T9 : IArrowArray + where TResult : IArrowArray + { + return (ArrowDelegate) + new ArrowUdfWrapper< + T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult>(udf).Execute; + } + + internal static Delegate CreateVectorUdfWrapper( + Func udf) + where T1 : IArrowArray + where T2 : IArrowArray + where T3 : IArrowArray + where T4 : IArrowArray + where T5 : IArrowArray + where T6 : IArrowArray + where T7 : IArrowArray + where T8 : IArrowArray + where T9 : IArrowArray + where T10 : IArrowArray + where TResult : IArrowArray + { + return (ArrowDelegate) + new ArrowUdfWrapper< + T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult>(udf).Execute; + } + } +} diff --git a/src/spark/Flowthru.Spark/Versions.cs b/src/spark/Flowthru.Spark/Versions.cs new file mode 100644 index 00000000..07807f9e --- /dev/null +++ b/src/spark/Flowthru.Spark/Versions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Flowthru.Spark +{ + internal static class Versions + { + internal const string V2_4_0 = "2.4.0"; + internal const string V2_4_2 = "2.4.2"; + internal const string V3_0_0 = "3.0.0"; + internal const string V3_1_0 = "3.1.0"; + internal const string V3_1_1 = "3.1.1"; + internal const string V3_2_0 = "3.2.0"; + internal const string V3_3_0 = "3.3.0"; + internal const string V3_5_1 = "3.5.1"; + } +} diff --git a/src/spark/Flowthru.Spark/build/netstandard2.0/Microsoft.Spark.targets b/src/spark/Flowthru.Spark/build/netstandard2.0/Microsoft.Spark.targets new file mode 100644 index 00000000..e2cdac9f --- /dev/null +++ b/src/spark/Flowthru.Spark/build/netstandard2.0/Microsoft.Spark.targets @@ -0,0 +1,7 @@ + + + + PreserveNewest + + + \ No newline at end of file diff --git a/src/spark/Flowthru.Spark/project.json b/src/spark/Flowthru.Spark/project.json new file mode 100644 index 00000000..2bf31df7 --- /dev/null +++ b/src/spark/Flowthru.Spark/project.json @@ -0,0 +1,8 @@ +{ + "name": "Flowthru.Spark", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "src/spark/Flowthru.Spark", + "tags": [ "lang:csharp", "scope:spark" ], + "implicitDependencies": [ "Flowthru.Spark.Jvm" ] +} diff --git a/tests/Flowthru.Analyzers.Tests/FSpark1002Tests.cs b/tests/Flowthru.Analyzers.Tests/FSpark1002Tests.cs new file mode 100644 index 00000000..04305106 --- /dev/null +++ b/tests/Flowthru.Analyzers.Tests/FSpark1002Tests.cs @@ -0,0 +1,228 @@ +using Flowthru.Extensions.Spark.Analyzers; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; + +namespace Flowthru.Analyzers.Tests; + +/// +/// Verifies that FSPARK1002 fires when a lambda inside a TypedFrameExtensions call +/// uses a string or Math method not in SparkTranslatableOperations, +/// and does not fire for supported methods. +/// +[TestFixture] +public class FSpark1002Tests +{ + private const string Stubs = """ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using Flowthru.Misc.DataFrames; + + namespace Flowthru.Misc.DataFrames + { + public class TypedFrame { } + + public static class TypedFrameExtensions + { + public static TypedFrame Where( + this TypedFrame source, + Expression> predicate) => null!; + + public static TypedFrame Select( + this TypedFrame source, + Expression> selector) => null!; + } + } + + public class PersonSchema { public string Name { get; set; } = ""; public double Score { get; set; } } + """; + + // ─── Negative cases: supported methods → no diagnostic ────────────────────── + + [Test] + public async Task SupportedStringMethod_ToUpper_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Where(x => x.Name.ToUpper() == "ALICE"); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task SupportedStringMethod_Contains_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Where(x => x.Name.Contains("Alice")); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task SupportedMathMethod_Round_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new { Rounded = Math.Round(x.Score, 2) }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + // ─── Positive cases: unsupported methods → FSPARK1002 fires ───────────────── + + [Test] + public async Task UnsupportedStringMethod_PadLeft_Reports_FSPARK1002() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Where(x => {|#0:x.Name.PadLeft(10)|} == " Alice"); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = + { + new DiagnosticResult(SparkDiagnostics.UnsupportedMethodCall) + .WithLocation(0) + .WithArguments( + "String", + "PadLeft", + Helpers.SupportedStringList, + Helpers.SupportedMathList + ), + }, + }.RunAsync(); + } + + [Test] + public async Task UnsupportedStringMethod_IndexOf_Reports_FSPARK1002() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new { Idx = {|#0:x.Name.IndexOf("needle")|} }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = + { + new DiagnosticResult(SparkDiagnostics.UnsupportedMethodCall) + .WithLocation(0) + .WithArguments( + "String", + "IndexOf", + Helpers.SupportedStringList, + Helpers.SupportedMathList + ), + }, + }.RunAsync(); + } + + [Test] + public async Task UnsupportedMathMethod_Pow_Reports_FSPARK1002() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new { Sq = {|#0:Math.Pow(x.Score, 2)|} }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = + { + new DiagnosticResult(SparkDiagnostics.UnsupportedMethodCall) + .WithLocation(0) + .WithArguments("Math", "Pow", Helpers.SupportedStringList, Helpers.SupportedMathList), + }, + }.RunAsync(); + } + + [Test] + public async Task NonFrameLambda_UnsupportedMethod_DoesNotReport() + { + // FSPARK1002 must not fire outside a TypedFrameExtensions call — same lambda shape + // but invoked on a plain List should be silent. + var source = + Stubs + + """ + + class Tests + { + void M(List list) + { + list.Where(x => x.Name.PadLeft(10) == " Alice"); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } +} diff --git a/tests/Flowthru.Analyzers.Tests/FdFrames1001Tests.cs b/tests/Flowthru.Analyzers.Tests/FdFrames1001Tests.cs new file mode 100644 index 00000000..7212a5bb --- /dev/null +++ b/tests/Flowthru.Analyzers.Tests/FdFrames1001Tests.cs @@ -0,0 +1,204 @@ +using Flowthru.Misc.DataFrames.Analyzers; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; + +namespace Flowthru.Analyzers.Tests; + +/// +/// Verifies that FDFRAMES1001 fires when a Select lambda body is not an object +/// initializer, positional record constructor, anonymous type, or member access — and +/// does not fire for those valid forms. +/// +[TestFixture] +public class FDFRAMES1001Tests +{ + // Minimal stubs that satisfy the analyzer's type-name check without requiring the + // full runtime library (which in turn requires the Spark JVM at runtime). + private const string Stubs = """ + using System; + using System.Linq.Expressions; + using Flowthru.Misc.DataFrames; + + namespace System.Runtime.CompilerServices + { + internal sealed class IsExternalInit { } + } + + namespace Flowthru.Misc.DataFrames + { + public class TypedFrame { } + + public static class TypedFrameExtensions + { + public static TypedFrame Select( + this TypedFrame source, + Expression> selector) => null!; + } + } + + public class InputSchema { public string Name { get; set; } = ""; public int Age { get; set; } } + public class OutputSchema + { + public OutputSchema() { } + public OutputSchema(string name, int age) { Name = name; Age = age; } + public string Name { get; set; } = ""; + public int Age { get; set; } + } + public record OutputRecord(string Name, int Age); + """; + + private static DiagnosticResult FDFRAMES1001(int line, int col) => + new DiagnosticResult(DataFrameDiagnostics.InvalidProjectionBody).WithLocation(line, col); + + // ─── Negative cases: valid projection bodies → no diagnostic ──────────────── + + [Test] + public async Task ObjectInitializer_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new OutputSchema { Name = x.Name, Age = x.Age }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task RecordPositionalConstructor_DoesNotReport() + { + // Uses an actual record so FDFRAMES1003 (positional ctor on non-record) does not fire + // — this test specifically verifies FDFRAMES1001 is silent for valid positional forms. + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new OutputRecord(x.Name, x.Age)); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task AnonymousTypeCreation_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new { x.Name, x.Age }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task SingleMemberAccess_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => x.Name); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + // ─── Positive cases: invalid projection bodies → FDFRAMES1001 fires ────────── + + [Test] + public async Task TupleCreate_Reports_FDFRAMES1001() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => {|#0:Tuple.Create(x.Name, x.Age)|}); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = + { + new DiagnosticResult(DataFrameDiagnostics.InvalidProjectionBody) + .WithLocation(0) + .WithArguments("Tuple.Create(x.Name, x.Age)"), + }, + }.RunAsync(); + } + + [Test] + public async Task ArbitraryMethodCall_Reports_FDFRAMES1001() + { + var source = + Stubs + + """ + + class Tests + { + static OutputSchema Map(InputSchema x) => new OutputSchema(); + + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => {|#0:Map(x)|}); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = + { + new DiagnosticResult(DataFrameDiagnostics.InvalidProjectionBody) + .WithLocation(0) + .WithArguments("Map(x)"), + }, + }.RunAsync(); + } +} diff --git a/tests/Flowthru.Analyzers.Tests/FdFrames1002Tests.cs b/tests/Flowthru.Analyzers.Tests/FdFrames1002Tests.cs new file mode 100644 index 00000000..bd72a5b3 --- /dev/null +++ b/tests/Flowthru.Analyzers.Tests/FdFrames1002Tests.cs @@ -0,0 +1,182 @@ +using Flowthru.Misc.DataFrames.Analyzers; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; + +namespace Flowthru.Analyzers.Tests; + +/// +/// Verifies that FDFRAMES1002 fires when an object initializer inside a Select lambda +/// uses a collection binding (Items = { x }) or nested-object binding +/// (Nested = { Prop = val }) rather than a plain property assignment. +/// +/// +/// The Select stub uses Func<> instead of Expression<Func<>> +/// to avoid the C# expression-tree restriction that prohibits collection and nested-object +/// initializer bindings in lambda-to-expression-tree conversions. The analyzer fires based on +/// the method's containing type name (TypedFrameExtensions), not the parameter type. +/// +[TestFixture] +public class FDFRAMES1002Tests +{ + // Stub Select takes Func<> so that the lambda body compiles without C# expression-tree + // restrictions while still triggering the TypedFrameExtensions name-match in the analyzer. + private const string Stubs = """ + using System; + using System.Collections.Generic; + using Flowthru.Misc.DataFrames; + + namespace Flowthru.Misc.DataFrames + { + public class TypedFrame { } + + public static class TypedFrameExtensions + { + public static TypedFrame Select( + this TypedFrame source, + Func selector) => null!; + } + } + + public class InputSchema { public string Name { get; set; } = ""; } + + public class OutputSchema + { + public string Label { get; set; } = ""; + public List Tags { get; set; } = new List(); + public NestedSchema Nested { get; set; } = new NestedSchema(); + } + + public class NestedSchema { public string Prop { get; set; } = ""; } + """; + + private static DiagnosticResult FDFRAMES1002(int marker) => + new DiagnosticResult(DataFrameDiagnostics.NonAssignmentBinding).WithLocation(marker); + + // ─── Negative cases: valid assignment bindings → no diagnostic ────────────── + + [Test] + public async Task AllAssignmentBindings_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new OutputSchema { Label = x.Name }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task AnonymousType_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new { x.Name }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + // ─── Positive cases: non-assignment bindings → FDFRAMES1002 fires ───────────── + + [Test] + public async Task CollectionBinding_Reports_FDFRAMES1002() + { + // Tags = { x.Name } is a MemberListBinding — RHS is a CollectionInitializerExpression. + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new OutputSchema { {|#0:Tags = { x.Name }|} }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = { FDFRAMES1002(0).WithArguments("Tags") }, + }.RunAsync(); + } + + [Test] + public async Task NestedObjectBinding_Reports_FDFRAMES1002() + { + // Nested = { Prop = x.Name } is a MemberMemberBinding — RHS is an ObjectInitializerExpression. + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new OutputSchema { {|#0:Nested = { Prop = x.Name }|} }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = { FDFRAMES1002(0).WithArguments("Nested") }, + }.RunAsync(); + } + + [Test] + public async Task MultipleNonAssignmentBindings_ReportsEach() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new OutputSchema + { + Label = x.Name, + {|#0:Tags = { x.Name }|}, + {|#1:Nested = { Prop = x.Name }|}, + }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = + { + FDFRAMES1002(0).WithArguments("Tags"), + FDFRAMES1002(1).WithArguments("Nested"), + }, + }.RunAsync(); + } +} diff --git a/tests/Flowthru.Analyzers.Tests/FdFrames1003Tests.cs b/tests/Flowthru.Analyzers.Tests/FdFrames1003Tests.cs new file mode 100644 index 00000000..7ca9fe3b --- /dev/null +++ b/tests/Flowthru.Analyzers.Tests/FdFrames1003Tests.cs @@ -0,0 +1,148 @@ +using Flowthru.Misc.DataFrames.Analyzers; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; + +namespace Flowthru.Analyzers.Tests; + +/// +/// Verifies that FDFRAMES1003 fires when a Select lambda uses a positional constructor +/// call on a plain class (not a record or anonymous type), and does not fire for records or +/// anonymous types. +/// +[TestFixture] +public class FDFRAMES1003Tests +{ + // Records require System.Runtime.CompilerServices.IsExternalInit, which the analyzer + // test framework doesn't inject automatically. This shim satisfies the requirement. + private const string Stubs = """ + using System; + using System.Linq.Expressions; + using Flowthru.Misc.DataFrames; + + namespace System.Runtime.CompilerServices + { + internal sealed class IsExternalInit { } + } + + namespace Flowthru.Misc.DataFrames + { + public class TypedFrame { } + + public static class TypedFrameExtensions + { + public static TypedFrame Select( + this TypedFrame source, + Expression> selector) => null!; + } + } + + public class InputSchema { public string Name { get; set; } = ""; public int Age { get; set; } } + + public class PlainClass + { + public PlainClass(string name, int age) { Name = name; Age = age; } + public string Name { get; } + public int Age { get; } + } + + public record PersonRecord(string Name, int Age); + """; + + private static DiagnosticResult FDFRAMES1003(int marker) => + new DiagnosticResult(DataFrameDiagnostics.PositionalConstructorNonRecord).WithLocation(marker); + + // ─── Negative cases: record and anonymous types → no diagnostic ───────────── + + [Test] + public async Task RecordPositionalConstructor_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new PersonRecord(x.Name, x.Age)); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task AnonymousType_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new { x.Name, x.Age }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task ObjectInitializer_PlainClass_DoesNotReport() + { + // FDFRAMES1003 only fires for positional constructors (args, no initializer). + // An object initializer on a plain class is valid. + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => new PlainClass(x.Name, x.Age) { }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + // ─── Positive case: positional constructor on a plain class → FDFRAMES1003 ─── + + [Test] + public async Task PlainClassPositionalConstructor_Reports_FDFRAMES1003() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.TypedFrame frame) + { + frame.Select(x => {|#0:new PlainClass(x.Name, x.Age)|}); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = { FDFRAMES1003(0).WithArguments("PlainClass") }, + }.RunAsync(); + } +} diff --git a/tests/Flowthru.Analyzers.Tests/FdFrames1004Tests.cs b/tests/Flowthru.Analyzers.Tests/FdFrames1004Tests.cs new file mode 100644 index 00000000..546e9a85 --- /dev/null +++ b/tests/Flowthru.Analyzers.Tests/FdFrames1004Tests.cs @@ -0,0 +1,171 @@ +using Flowthru.Misc.DataFrames.Analyzers; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; + +namespace Flowthru.Analyzers.Tests; + +/// +/// Verifies that FDFRAMES1004 fires when the Aggregate result selector body is not an +/// object-creation expression, and does not fire for valid object-creation forms. +/// +[TestFixture] +public class FDFRAMES1004Tests +{ + private const string Stubs = """ + using System; + using System.Linq.Expressions; + using Flowthru.Misc.DataFrames; + + namespace System.Runtime.CompilerServices + { + internal sealed class IsExternalInit { } + } + + namespace Flowthru.Misc.DataFrames + { + public class TypedFrame { } + public class GroupedFrame { } + + public sealed class AggregationContext + { + private AggregationContext() { } + public TKey Key => throw new InvalidOperationException(); + public double Avg(Expression> column) => throw new InvalidOperationException(); + public double Sum(Expression> column) => throw new InvalidOperationException(); + public long Count() => throw new InvalidOperationException(); + } + + public static class GroupedFrameExtensions + { + public static TypedFrame Aggregate( + this GroupedFrame source, + Expression, TResult>> resultSelector) => null!; + } + } + + public class ProductSchema { public string Category { get; set; } = ""; public double Price { get; set; } } + public class AggResult { public string Category { get; set; } = ""; public double AvgPrice { get; set; } } + public record AggResultRecord(string Category, double AvgPrice); + """; + + private static DiagnosticResult FDFRAMES1004(int marker) => + new DiagnosticResult(DataFrameDiagnostics.InvalidAggregateResultBody).WithLocation(marker); + + // ─── Negative cases: object-creation forms → no diagnostic ────────────────── + + [Test] + public async Task ObjectInitializer_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => new AggResult { Category = ctx.Key, AvgPrice = ctx.Avg(x => x.Price) }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task AnonymousType_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => new { Category = ctx.Key, AvgPrice = ctx.Avg(x => x.Price) }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task RecordPositionalConstructor_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => new AggResultRecord(ctx.Key, ctx.Avg(x => x.Price))); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + // ─── Positive cases: non-object-creation bodies → FDFRAMES1004 fires ───────── + + [Test] + public async Task MemberAccessBody_Reports_FDFRAMES1004() + { + // ctx.Key is a member access, not an object-creation expression. + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => {|#0:ctx.Key|}); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = { FDFRAMES1004(0).WithArguments("ctx.Key") }, + }.RunAsync(); + } + + [Test] + public async Task InvocationBody_Reports_FDFRAMES1004() + { + // ctx.Count() is a method call, not an object-creation expression. + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => {|#0:ctx.Count()|}); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = { FDFRAMES1004(0).WithArguments("ctx.Count()") }, + }.RunAsync(); + } +} diff --git a/tests/Flowthru.Analyzers.Tests/FdFrames1005Tests.cs b/tests/Flowthru.Analyzers.Tests/FdFrames1005Tests.cs new file mode 100644 index 00000000..d2d657c2 --- /dev/null +++ b/tests/Flowthru.Analyzers.Tests/FdFrames1005Tests.cs @@ -0,0 +1,252 @@ +using Flowthru.Misc.DataFrames.Analyzers; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; + +namespace Flowthru.Analyzers.Tests; + +/// +/// Verifies that FDFRAMES1005 fires when an Aggregate result selector binding is +/// neither ctx.Key nor a call to an aggregation method on the context, and does not +/// fire for valid bindings. +/// +[TestFixture] +public class FDFRAMES1005Tests +{ + private const string Stubs = """ + using System; + using System.Linq.Expressions; + using Flowthru.Misc.DataFrames; + + namespace Flowthru.Misc.DataFrames + { + public class TypedFrame { } + public class GroupedFrame { } + + public sealed class AggregationContext + { + private AggregationContext() { } + public TKey Key => throw new InvalidOperationException(); + public double Avg(Expression> column) => throw new InvalidOperationException(); + public double Sum(Expression> column) => throw new InvalidOperationException(); + public long Count() => throw new InvalidOperationException(); + } + + public static class GroupedFrameExtensions + { + public static TypedFrame Aggregate( + this GroupedFrame source, + Expression, TResult>> resultSelector) => null!; + } + } + + public class ProductSchema { public string Category { get; set; } = ""; public double Price { get; set; } } + public class AggResult + { + public string Category { get; set; } = ""; + public double AvgPrice { get; set; } + public long RowCount { get; set; } + } + """; + + private static DiagnosticResult FDFRAMES1005(int marker) => + new DiagnosticResult(DataFrameDiagnostics.InvalidAggregateBinding).WithLocation(marker); + + // ─── Negative cases: valid ctx.Key / ctx.Method() bindings → no diagnostic ── + + [Test] + public async Task KeyAndAvg_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => new AggResult + { + Category = ctx.Key, + AvgPrice = ctx.Avg(x => x.Price), + RowCount = ctx.Count(), + }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task AnonymousType_ValidBindings_DoesNotReport() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => new { Category = ctx.Key, Avg = ctx.Avg(x => x.Price) }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + [Test] + public async Task AnonymousType_ShorthandKeyAccess_DoesNotReport() + { + // new { ctx.Key } is shorthand — d.Expression is ctx.Key, a MemberAccessExpression on ctx. + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => new { ctx.Key }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + }.RunAsync(); + } + + // ─── Positive cases: non-ctx bindings → FDFRAMES1005 fires ─────────────────── + + [Test] + public async Task LiteralBinding_Reports_FDFRAMES1005() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => new AggResult { Category = ctx.Key, AvgPrice = {|#0:42.0|} }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = { FDFRAMES1005(0).WithArguments("42.0") }, + }.RunAsync(); + } + + [Test] + public async Task StringConstantBinding_Reports_FDFRAMES1005() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => new AggResult { Category = {|#0:"hardcoded"|} }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = { FDFRAMES1005(0).WithArguments("\"hardcoded\"") }, + }.RunAsync(); + } + + [Test] + public async Task BinaryExpressionOnKey_Reports_FDFRAMES1005() + { + // ctx.Key + "_suffix" is a BinaryExpression, not a direct member access on ctx. + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => new AggResult { Category = {|#0:ctx.Key + "_suffix"|} }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = { FDFRAMES1005(0).WithArguments("ctx.Key + \"_suffix\"") }, + }.RunAsync(); + } + + [Test] + public async Task ChainedMemberAccess_Reports_FDFRAMES1005() + { + // ctx.Key.Length accesses a member on the result of ctx.Key, not on ctx directly. + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => new { Length = {|#0:ctx.Key.Length|} }); + } + } + """; + + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = { FDFRAMES1005(0).WithArguments("ctx.Key.Length") }, + }.RunAsync(); + } + + [Test] + public async Task MixedBindings_ReportsOnlyInvalid() + { + var source = + Stubs + + """ + + class Tests + { + void M(Flowthru.Misc.DataFrames.GroupedFrame grouped) + { + grouped.Aggregate(ctx => new AggResult + { + Category = ctx.Key, + AvgPrice = ctx.Avg(x => x.Price), + RowCount = {|#0:(long)ctx.Count()|}, + }); + } + } + """; + + // (long)ctx.Count() is a CastExpression — not a direct ctx.Method() call. + await new CSharpAnalyzerTest + { + TestCode = source, + ExpectedDiagnostics = { FDFRAMES1005(0).WithArguments("(long)ctx.Count()") }, + }.RunAsync(); + } +} diff --git a/tests/Flowthru.Analyzers.Tests/Flowthru.Analyzers.Tests.csproj b/tests/Flowthru.Analyzers.Tests/Flowthru.Analyzers.Tests.csproj new file mode 100644 index 00000000..93425a1f --- /dev/null +++ b/tests/Flowthru.Analyzers.Tests/Flowthru.Analyzers.Tests.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Flowthru.Analyzers.Tests/Helpers.cs b/tests/Flowthru.Analyzers.Tests/Helpers.cs new file mode 100644 index 00000000..79212ee6 --- /dev/null +++ b/tests/Flowthru.Analyzers.Tests/Helpers.cs @@ -0,0 +1,20 @@ +using Flowthru.Extensions.Spark.Shared; + +namespace Flowthru.Analyzers.Tests; + +/// +/// Shared test helpers derived from so that +/// expected diagnostic message arguments stay in sync with the analyzer automatically. +/// +internal static class Helpers +{ + public static readonly string SupportedStringList = string.Join( + ", ", + SparkTranslatableOperations.SupportedStringMethods + ); + + public static readonly string SupportedMathList = string.Join( + ", ", + SparkTranslatableOperations.SupportedMathMethods + ); +} diff --git a/tests/Flowthru.Analyzers.Tests/NUnit4Verifier.cs b/tests/Flowthru.Analyzers.Tests/NUnit4Verifier.cs new file mode 100644 index 00000000..550a0f97 --- /dev/null +++ b/tests/Flowthru.Analyzers.Tests/NUnit4Verifier.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; + +namespace Flowthru.Analyzers.Tests; + +/// +/// An implementation compatible with NUnit 4.x. +/// +/// +/// Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit 1.1.2 ships a +/// NUnitVerifier built against NUnit 3 API, which is binary-incompatible with +/// NUnit 4. This verifier reimplements the same interface against NUnit 4 assertion APIs. +/// +internal sealed class NUnit4Verifier : IVerifier +{ + private readonly string _context; + + public NUnit4Verifier() + : this(string.Empty) { } + + private NUnit4Verifier(string context) + { + _context = context; + } + + public void Empty(string collectionName, IEnumerable collection) + { + var list = collection.ToList(); + Assert.That(list, Is.Empty, FormatMessage($"'{collectionName}' should be empty.")); + } + + public void NotEmpty(string collectionName, IEnumerable collection) + { + var list = collection.ToList(); + Assert.That(list, Is.Not.Empty, FormatMessage($"'{collectionName}' should not be empty.")); + } + + public void LanguageIsSupported(string language) + { + Assert.That( + language, + Is.EqualTo("C#"), + FormatMessage($"Language '{language}' is not supported by this verifier (expected C#).") + ); + } + + public void Equal(T expected, T actual, string? message = null) + { + Assert.That(actual, Is.EqualTo(expected), FormatMessage(message ?? string.Empty)); + } + + public void True(bool assert, string? message = null) + { + Assert.That(assert, Is.True, FormatMessage(message ?? string.Empty)); + } + + public void False(bool assert, string? message = null) + { + Assert.That(assert, Is.False, FormatMessage(message ?? string.Empty)); + } + + public void SequenceEqual( + IEnumerable expected, + IEnumerable actual, + IEqualityComparer? comparer = null, + string? message = null + ) + { + var expectedList = expected.ToList(); + var actualList = actual.ToList(); + Assert.That( + actualList, + Is.EqualTo(expectedList).Using(comparer ?? EqualityComparer.Default), + FormatMessage(message ?? string.Empty) + ); + } + + [DoesNotReturn] + public void Fail(string? message = null) + { + Assert.Fail(FormatMessage(message ?? string.Empty)); + throw new InvalidOperationException("unreachable"); + } + + public IVerifier PushContext(string context) + { + var combined = string.IsNullOrEmpty(_context) ? context : $"{_context} > {context}"; + return new NUnit4Verifier(combined); + } + + private string FormatMessage(string message) => + string.IsNullOrEmpty(_context) ? message : $"[{_context}] {message}"; +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/ColumnNameResolutionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/ColumnNameResolutionTests.cs new file mode 100644 index 00000000..7b785f3a --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/ColumnNameResolutionTests.cs @@ -0,0 +1,112 @@ +using System.Reflection; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class ColumnNameResolutionTests +{ + // =================================================================== + // ResolveColumnName via FrameExpressionVisitor (protected static, + // tested indirectly via a minimal subclass) + // =================================================================== + + [Test] + public void ResolveColumnName_WithoutSerializedLabel_ReturnsPropertyName() + { + var member = typeof(PersonSchema).GetProperty(nameof(PersonSchema.Name))!; + + var result = TestableVisitor.TestResolveColumnName(member); + + Assert.That(result, Is.EqualTo("Name")); + } + + [Test] + public void ResolveColumnName_WithSerializedLabel_ReturnsLabelValue() + { + var member = typeof(LabeledSchema).GetProperty(nameof(LabeledSchema.FullName))!; + + var result = TestableVisitor.TestResolveColumnName(member); + + Assert.That(result, Is.EqualTo("full_name")); + } + + [Test] + public void ResolveColumnName_PropertyWithoutLabel_FallsBackToPropertyName() + { + var member = typeof(LabeledSchema).GetProperty(nameof(LabeledSchema.Department))!; + + var result = TestableVisitor.TestResolveColumnName(member); + + Assert.That(result, Is.EqualTo("Department")); + } + + [Test] + public void ResolveColumnName_AllLabeledProperties_ResolveCorrectly() + { + var props = typeof(LabeledSchema).GetProperties(); + var expected = new Dictionary + { + ["FullName"] = "full_name", + ["EmployeeId"] = "employee_id", + ["Department"] = "Department", + }; + + foreach (var prop in props) + { + var resolved = TestableVisitor.TestResolveColumnName(prop); + Assert.That(resolved, Is.EqualTo(expected[prop.Name]), $"Failed for property {prop.Name}"); + } + } + + // =================================================================== + // Minimal subclass exposing protected static methods for testing + // =================================================================== + + private class TestableVisitor : FrameExpressionVisitor + { + public static string TestResolveColumnName(MemberInfo member) => ResolveColumnName(member); + + protected override object TranslateConstant(System.Linq.Expressions.ConstantExpression node) => + throw new NotImplementedException(); + + protected override object TranslateWhere(System.Linq.Expressions.MethodCallExpression node) => + throw new NotImplementedException(); + + protected override object TranslateSelect(System.Linq.Expressions.MethodCallExpression node) => + throw new NotImplementedException(); + + protected override object TranslateJoin(System.Linq.Expressions.MethodCallExpression node) => + throw new NotImplementedException(); + + protected override object TranslateOrderBy( + System.Linq.Expressions.MethodCallExpression node, + bool descending + ) => throw new NotImplementedException(); + + protected override object TranslateTake(System.Linq.Expressions.MethodCallExpression node) => + throw new NotImplementedException(); + + protected override object TranslateCount(System.Linq.Expressions.MethodCallExpression node) => + throw new NotImplementedException(); + + protected override object TranslateDistinct( + System.Linq.Expressions.MethodCallExpression node + ) => throw new NotImplementedException(); + + protected override object TranslateUnion(System.Linq.Expressions.MethodCallExpression node) => + throw new NotImplementedException(); + + protected override object TranslateGroupBy(System.Linq.Expressions.MethodCallExpression node) => + throw new NotImplementedException(); + + protected override object TranslateAggregate( + System.Linq.Expressions.MethodCallExpression node + ) => throw new NotImplementedException(); + + protected override object TranslateSelectOver( + System.Linq.Expressions.MethodCallExpression node + ) => throw new NotImplementedException(); + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/CompatTests/SparkRowHydratorExecutionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/CompatTests/SparkRowHydratorExecutionTests.cs new file mode 100644 index 00000000..c9e18798 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/CompatTests/SparkRowHydratorExecutionTests.cs @@ -0,0 +1,206 @@ +using Flowthru.Core.Abstractions; +using Flowthru.Misc.DataFrames; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Extensions.Spark.Tests.CompatTests; + +/// +/// End-to-end tests for that require a live JVM. +/// These tests exercise the full pipeline: TypedFrame → CompileToNative → ValidateSchema +/// → Collect → hydrated rows. +/// +/// Automatically skipped (Inconclusive) when SPARK_HOME is not set. +/// +/// To run locally: +/// brew install apache-spark +/// export SPARK_HOME=$(brew --prefix apache-spark)/libexec +/// dotnet test --filter "Category=SparkRowHydrator.Execution" +/// +[TestFixture] +[Category("SparkRowHydrator")] +[Category("SparkRowHydrator.Execution")] +public class SparkRowHydratorExecutionTests +{ + // SparkSession is stopped via .Stop() rather than IDisposable; NUnit1032 suppressed. +#pragma warning disable NUnit1032 + private SparkSession? _spark; +#pragma warning restore NUnit1032 + private SparkFrameProvider? _provider; + + [OneTimeSetUp] + public void StartSpark() + { + Assume.That( + SparkAssemblySetup.IsAvailable, + Is.True, + SparkAssemblySetup.UnavailableReason ?? "Spark JVM backend unavailable." + ); + + try + { + _spark = SparkSession + .Builder() + .AppName("flowthru-hydrator-tests") + .Master("local[*]") + .GetOrCreate(); + + _provider = new SparkFrameProvider(); + } + catch (Exception ex) + { + Assert.Inconclusive( + $"Spark JVM backend failed to start — skipping hydrator execution tests. ({ex.Message})" + ); + } + } + + [OneTimeTearDown] + public void StopSpark() + { + _spark?.Stop(); + } + + // Convenience: build a DataFrame with PersonSchema-compatible columns + private DataFrame PersonDataFrame(params (string name, int age, bool isActive)[] rows) + { + var schema = new StructType( + [ + new StructField("Name", new StringType()), + new StructField("Age", new IntegerType()), + new StructField("IsActive", new BooleanType()), + ] + ); + + var genericRows = rows.Select(r => new GenericRow([r.name, r.age, r.isActive])); + return _spark!.CreateDataFrame(genericRows, schema); + } + + // =================================================================== + // ValidateSchema(DataFrame) — outer overload with live JVM + // =================================================================== + + [Test] + public void ValidateSchema_DataFrame_CompatibleSchema_ReturnsEmpty() + { + var hydrator = new SparkRowHydrator(_provider!); + var df = PersonDataFrame(("Alice", 30, true)); + + var errors = hydrator.ValidateSchema(df); + + Assert.That(errors, Is.Empty); + } + + [Test] + public void ValidateSchema_DataFrame_MissingColumn_ReturnsError() + { + var hydrator = new SparkRowHydrator(_provider!); + + // DataFrame has only Name and Age — IsActive is missing + var schema = new StructType( + [new StructField("Name", new StringType()), new StructField("Age", new IntegerType())] + ); + var df = _spark!.CreateDataFrame([new GenericRow(["Alice", 30])], schema); + + var errors = hydrator.ValidateSchema(df); + + Assert.That(errors, Has.Count.EqualTo(1)); + Assert.That(errors[0].ColumnName, Is.EqualTo("IsActive")); + } + + // =================================================================== + // Collect — full round-trip + // =================================================================== + + [Test] + public void Collect_ReturnsAllRows() + { + var hydrator = new SparkRowHydrator(_provider!); + var df = PersonDataFrame(("Alice", 30, true), ("Bob", 25, false), ("Carol", 40, true)); + var frame = _provider!.CreateFromNative(df); + + var rows = hydrator.Collect(frame).ToList(); + + Assert.That(rows, Has.Count.EqualTo(3)); + } + + [Test] + public void Collect_HydratesPropertyValues() + { + var hydrator = new SparkRowHydrator(_provider!); + var df = PersonDataFrame(("Alice", 30, true)); + var frame = _provider!.CreateFromNative(df); + + var rows = hydrator.Collect(frame).ToList(); + var alice = rows.Single(); + + Assert.That(alice.Name, Is.EqualTo("Alice")); + Assert.That(alice.Age, Is.EqualTo(30)); + Assert.That(alice.IsActive, Is.True); + } + + [Test] + public void Collect_AfterWhere_FiltersRows() + { + var hydrator = new SparkRowHydrator(_provider!); + var df = PersonDataFrame(("Alice", 30, true), ("Bob", 17, false), ("Carol", 40, true)); + var frame = + (TypedFrame) + _provider!.CreateFromNative(df).Where(x => x.Age > 18); + + var rows = hydrator.Collect(frame).ToList(); + + Assert.That(rows, Has.Count.EqualTo(2)); + Assert.That(rows.Select(r => r.Name), Is.EquivalentTo(new[] { "Alice", "Carol" })); + } + + [Test] + public void Collect_SchemaMismatch_ThrowsBeforeCollect() + { + var hydrator = new SparkRowHydrator(_provider!); + + // DataFrame has Age as StringType — incompatible with int property + var schema = new StructType( + [ + new StructField("Name", new StringType()), + new StructField("Age", new StringType()), // wrong type + new StructField("IsActive", new BooleanType()), + ] + ); + var df = _spark!.CreateDataFrame([new GenericRow(["Alice", "thirty", true])], schema); + var frame = _provider!.CreateFromNative(df); + + Assert.That( + () => hydrator.Collect(frame).ToList(), + Throws.TypeOf().With.Message.Contains("Age") + ); + } + + // =================================================================== + // [SerializedLabel] round-trip + // =================================================================== + + [Test] + public void Collect_HonoursSerializedLabel_InColumnLookup() + { + var hydrator = new SparkRowHydrator(_provider!); + + // Columns use the serialized names (snake_case), not C# property names + var schema = new StructType( + [ + new StructField("full_name", new StringType()), + new StructField("employee_id", new IntegerType()), + new StructField("Department", new StringType()), + ] + ); + var df = _spark!.CreateDataFrame([new GenericRow(["Alice Smith", 42, "Engineering"])], schema); + var frame = _provider!.CreateFromNative(df); + + var rows = hydrator.Collect(frame).ToList(); + var row = rows.Single(); + + Assert.That(row.FullName, Is.EqualTo("Alice Smith")); + Assert.That(row.EmployeeId, Is.EqualTo(42)); + Assert.That(row.Department, Is.EqualTo("Engineering")); + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/CompatTests/SparkSessionSmokeTests.cs b/tests/Flowthru.Extensions.Spark.Tests/CompatTests/SparkSessionSmokeTests.cs new file mode 100644 index 00000000..82449ce9 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/CompatTests/SparkSessionSmokeTests.cs @@ -0,0 +1,123 @@ +using Flowthru.Spark.Sql; + +namespace Flowthru.Extensions.Spark.Tests.CompatTests; + +/// +/// Smoke tests validating Flowthru.Spark compatibility with the current target framework. +/// Layer 1 tests verify assembly loading (no JVM required). +/// Layer 2 tests verify JVM bridge functionality (requires Java, Spark, and Flowthru.Spark.Worker). +/// +[TestFixture] +[Category("SparkSession")] +public class SparkSessionSmokeTests +{ + // ================================================================= + // Layer 1 — Assembly Loading (no JVM required) + // + // Builder's constructor immediately calls into the JVM bridge, + // so Layer 1 can only verify that types resolve via reflection — + // not instantiate them. + // ================================================================= + + [Test] + [Category("SparkSession.AssemblyLoading")] + public void SparkSession_TypeLoads_OnCurrentFramework() + { + // If Microsoft.Spark types can't resolve on this TFM, this will throw + // TypeLoadException or FileLoadException before reaching the assert. + var sessionType = typeof(SparkSession); + + Assert.That(sessionType, Is.Not.Null); + Assert.That(sessionType.AssemblyQualifiedName, Does.Contain("Flowthru.Spark")); + } + + [Test] + [Category("SparkSession.AssemblyLoading")] + public void Builder_TypeLoads_OnCurrentFramework() + { + // Builder is a top-level class in Microsoft.Spark.Sql, not nested. + var builderType = typeof(Builder); + + Assert.That(builderType, Is.Not.Null); + Assert.That(builderType.AssemblyQualifiedName, Does.Contain("Flowthru.Spark")); + } + + [Test] + [Category("SparkSession.AssemblyLoading")] + public void DataFrame_TypeLoads_OnCurrentFramework() + { + var dfType = typeof(DataFrame); + + Assert.That(dfType, Is.Not.Null); + Assert.That(dfType.AssemblyQualifiedName, Does.Contain("Flowthru.Spark")); + } + + [Test] + [Category("SparkSession.AssemblyLoading")] + public void Builder_HasExpectedFluentApiMethods() + { + // Verify the API surface we depend on exists at the reflection level. + var builderType = typeof(Builder); + + Assert.That(builderType.GetMethod("Master", [typeof(string)]), Is.Not.Null); + Assert.That(builderType.GetMethod("AppName", [typeof(string)]), Is.Not.Null); + Assert.That(builderType.GetMethod("GetOrCreate"), Is.Not.Null); + } + + // ================================================================= + // Layer 2 — JVM Bridge (requires Java + Spark + Worker) + // + // These tests are ignored by default. Remove the [Ignore] attribute + // when running with a full Spark environment configured: + // - Java 1.8+ + // - SPARK_HOME set to a Spark distribution + // - DOTNET_WORKER_DIR set to extracted Microsoft.Spark.Worker + // ================================================================= + + [Test] + [Ignore("Requires JVM + Spark + Microsoft.Spark.Worker runtime environment")] + [Category("SparkSession.JvmBridge")] + public void SparkSession_GetOrCreate_EstablishesJvmBridge() + { + SparkSession? spark = null; + try + { + spark = SparkSession + .Builder() + .AppName("net10-jvm-bridge-test") + .Master("local[*]") + .GetOrCreate(); + + Assert.That(spark, Is.Not.Null); + } + finally + { + spark?.Stop(); + } + } + + [Test] + [Ignore("Requires JVM + Spark + Microsoft.Spark.Worker runtime environment")] + [Category("SparkSession.JvmBridge")] + public void SparkSession_Range_ExecutesTrivialDataFrameOperation() + { + SparkSession? spark = null; + try + { + spark = SparkSession + .Builder() + .AppName("net10-dataframe-test") + .Master("local[*]") + .GetOrCreate(); + + var df = spark.Range(10); + var count = df.Count(); + + Assert.That(count, Is.EqualTo(10)); + } + finally + { + spark?.Stop(); + } + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/ConditionalExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/ConditionalExpressionTests.cs new file mode 100644 index 00000000..c6c3cd9c --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/ConditionalExpressionTests.cs @@ -0,0 +1,108 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class ConditionalExpressionTests +{ + private TestFrameProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // Conditional expression in Select projection + // =================================================================== + + [Test] + public void Select_WithTernaryInBody_ContainsConditionalExpression() + { + var frame = new TypedFrame(_provider); + + // x.Age >= 18 ? "adult" : "minor" + var result = frame.Select(x => new NameOnlySchema { Name = x.Age >= 18 ? "adult" : "minor" }); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var mie = (MemberInitExpression)lambda.Body; + var binding = mie.Bindings.OfType().Single(); + + Assert.That(binding.Expression.NodeType, Is.EqualTo(ExpressionType.Conditional)); + } + + [Test] + public void Select_WithTernary_ConditionalExpression_HasCorrectTestNodeType() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new NameOnlySchema { Name = x.Age >= 18 ? "adult" : "minor" }); + + var conditional = ExtractConditionalFromSelect(result); + + // Test: x.Age >= 18 — binary GreaterThanOrEqual + Assert.That(conditional.Test.NodeType, Is.EqualTo(ExpressionType.GreaterThanOrEqual)); + } + + [Test] + public void Select_WithTernary_IfTrue_IsStringConstant() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new NameOnlySchema { Name = x.Age >= 18 ? "adult" : "minor" }); + + var conditional = ExtractConditionalFromSelect(result); + + var ifTrueConst = conditional.IfTrue as ConstantExpression; + Assert.That(ifTrueConst, Is.Not.Null); + Assert.That(ifTrueConst!.Value, Is.EqualTo("adult")); + } + + [Test] + public void Select_WithTernary_IfFalse_IsStringConstant() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new NameOnlySchema { Name = x.Age >= 18 ? "adult" : "minor" }); + + var conditional = ExtractConditionalFromSelect(result); + + var ifFalseConst = conditional.IfFalse as ConstantExpression; + Assert.That(ifFalseConst, Is.Not.Null); + Assert.That(ifFalseConst!.Value, Is.EqualTo("minor")); + } + + [Test] + public void Where_WithTernaryConvertedToBool_ContainsConditionalExpression() + { + // Although unusual, a conditional can appear inside a where predicate + // via a bool expression: x.IsActive ? x.Age > 18 : false + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.IsActive ? x.Age > 18 : false); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + + Assert.That(lambda.Body.NodeType, Is.EqualTo(ExpressionType.Conditional)); + } + + // =================================================================== + // Helpers + // =================================================================== + + private static ConditionalExpression ExtractConditionalFromSelect( + TypedFrame frame + ) + { + var mce = (MethodCallExpression)frame.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var mie = (MemberInitExpression)lambda.Body; + var binding = mie.Bindings.OfType().Single(); + return (ConditionalExpression)binding.Expression; + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/DistinctUnionExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/DistinctUnionExpressionTests.cs new file mode 100644 index 00000000..f93276f9 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/DistinctUnionExpressionTests.cs @@ -0,0 +1,136 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class DistinctUnionExpressionTests +{ + private TestFrameProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // Distinct + // =================================================================== + + [Test] + public void Distinct_ProducesMethodCallExpression_WithCorrectMethodName() + { + var frame = new TypedFrame(_provider); + + var result = frame.Distinct(); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("Distinct")); + } + + [Test] + public void Distinct_PreservesSourceTypeParameter() + { + var frame = new TypedFrame(_provider); + + var result = frame.Distinct(); + + Assert.That(result.ElementType, Is.EqualTo(typeof(PersonSchema))); + } + + [Test] + public void Distinct_HasOneArgument_TheSourceExpression() + { + var frame = new TypedFrame(_provider); + + var result = frame.Distinct(); + + var mce = (MethodCallExpression)result.Expression; + Assert.That(mce.Arguments, Has.Count.EqualTo(1)); + } + + [Test] + public void Distinct_CanChainAfterWhere() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.IsActive).Distinct(); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce!.Method.Name, Is.EqualTo("Distinct")); + + var innerMce = mce.Arguments[0] as MethodCallExpression; + Assert.That(innerMce!.Method.Name, Is.EqualTo("Where")); + } + + // =================================================================== + // Union + // =================================================================== + + [Test] + public void Union_ProducesMethodCallExpression_WithCorrectMethodName() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Union(right); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("Union")); + } + + [Test] + public void Union_PreservesSourceTypeParameter() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Union(right); + + Assert.That(result.ElementType, Is.EqualTo(typeof(PersonSchema))); + } + + [Test] + public void Union_HasTwoArguments_LeftAndRight() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Union(right); + + var mce = (MethodCallExpression)result.Expression; + Assert.That(mce.Arguments, Has.Count.EqualTo(2)); + } + + [Test] + public void Union_BothArguments_AreConstantExpressions_ForRootFrames() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Union(right); + + var mce = (MethodCallExpression)result.Expression; + Assert.That(mce.Arguments[0], Is.InstanceOf()); + Assert.That(mce.Arguments[1], Is.InstanceOf()); + } + + [Test] + public void Union_CanChainWithDistinct() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Union(right).Distinct(); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce!.Method.Name, Is.EqualTo("Distinct")); + + var innerMce = mce.Arguments[0] as MethodCallExpression; + Assert.That(innerMce!.Method.Name, Is.EqualTo("Union")); + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/Flowthru.Extensions.Spark.Tests.csproj b/tests/Flowthru.Extensions.Spark.Tests/Flowthru.Extensions.Spark.Tests.csproj new file mode 100644 index 00000000..b387c886 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/Flowthru.Extensions.Spark.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/tests/Flowthru.Extensions.Spark.Tests/FrameItemFactoryTests.cs b/tests/Flowthru.Extensions.Spark.Tests/FrameItemFactoryTests.cs new file mode 100644 index 00000000..0fa92cf4 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/FrameItemFactoryTests.cs @@ -0,0 +1,88 @@ +using Flowthru.Core.Data; +using Flowthru.Extensions.Spark; +using Flowthru.Extensions.Spark.Data; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("FrameItemFactory")] +public class FrameItemFactoryTests +{ + // =================================================================== + // ItemFactory.Frame property + // =================================================================== + + [Test] + public void ItemFactory_Frame_IsNotNull() + { + Assert.That(ItemFactory.Frame, Is.Not.Null); + } + + [Test] + public void ItemFactory_Frame_IsFrameItemFactory() + { + Assert.That(ItemFactory.Frame, Is.InstanceOf()); + } + + // =================================================================== + // Memory factory + // =================================================================== + + [Test] + public void Frame_Memory_ReturnsItemOfTypedFrame() + { + var item = ItemFactory.Frame.Memory("test"); + + Assert.That(item, Is.Not.Null); + Assert.That(item, Is.InstanceOf>>()); + } + + [Test] + public void Frame_Memory_PreservesLabel() + { + var item = ItemFactory.Frame.Memory("my-frame"); + + Assert.That(item.Label, Is.EqualTo("my-frame")); + } + + [Test] + public void Frame_Memory_StorageTraits_IsNotPersistent() + { + var item = ItemFactory.Frame.Memory("test"); + + Assert.That(item.Traits.IsPersistent, Is.False); + } + + // =================================================================== + // Save → Load round-trip: no serialization, same reference + // =================================================================== + + [Test] + public async Task Frame_Memory_SaveThenLoad_ReturnsSameReference() + { + var item = ItemFactory.Frame.Memory("test"); + var provider = new TestFrameProvider(); + var frame = new TypedFrame(provider); + + await item.Save(frame).Run(); + var loaded = await item.Load().Run(); + + Assert.That(loaded, Is.SameAs(frame)); + } + + [Test] + public async Task Frame_Memory_LoadAfterTwoSaves_ReturnsLastSavedReference() + { + var item = ItemFactory.Frame.Memory("test"); + var provider = new TestFrameProvider(); + var first = new TypedFrame(provider); + var second = new TypedFrame(provider); + + await item.Save(first).Run(); + await item.Save(second).Run(); + var loaded = await item.Load().Run(); + + Assert.That(loaded, Is.SameAs(second)); + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/GroupByAggregateExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/GroupByAggregateExpressionTests.cs new file mode 100644 index 00000000..ab52affc --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/GroupByAggregateExpressionTests.cs @@ -0,0 +1,238 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class GroupByAggregateExpressionTests +{ + private TestFrameProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // GroupBy tree structure + // =================================================================== + + [Test] + public void GroupBy_ProducesMethodCallExpression_WithCorrectMethodName() + { + var frame = new TypedFrame(_provider); + + var result = frame.GroupBy(x => x.Category); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("GroupBy")); + } + + [Test] + public void GroupBy_HasTwoArguments_SourceAndKeySelector() + { + var frame = new TypedFrame(_provider); + + var result = frame.GroupBy(x => x.Category); + + var mce = (MethodCallExpression)result.Expression; + Assert.That(mce.Arguments, Has.Count.EqualTo(2)); + } + + [Test] + public void GroupBy_KeySelector_IsQuotedLambda() + { + var frame = new TypedFrame(_provider); + + var result = frame.GroupBy(x => x.Category); + + var mce = (MethodCallExpression)result.Expression; + var quoted = mce.Arguments[1] as UnaryExpression; + Assert.That(quoted, Is.Not.Null); + Assert.That(quoted!.NodeType, Is.EqualTo(ExpressionType.Quote)); + Assert.That(quoted.Operand, Is.InstanceOf()); + } + + [Test] + public void GroupBy_KeySelector_BodyIsMemberAccess_ForKeyProperty() + { + var frame = new TypedFrame(_provider); + + var result = frame.GroupBy(x => x.Category); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var member = lambda.Body as MemberExpression; + Assert.That(member, Is.Not.Null); + Assert.That(member!.Member.Name, Is.EqualTo(nameof(SalesSchema.Category))); + } + + // =================================================================== + // Aggregate tree structure + // =================================================================== + + [Test] + public void Aggregate_ProducesMethodCallExpression_WithCorrectMethodName() + { + var frame = new TypedFrame(_provider); + + var result = frame + .GroupBy(x => x.Category) + .Aggregate(ctx => new SalesSummarySchema + { + Category = ctx.Key, + TotalAmount = ctx.Sum(x => x.Amount), + TotalCount = ctx.Count(), + }); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("Aggregate")); + } + + [Test] + public void Aggregate_ProducesNewResultType() + { + var frame = new TypedFrame(_provider); + + var result = frame + .GroupBy(x => x.Category) + .Aggregate(ctx => new SalesSummarySchema + { + Category = ctx.Key, + TotalAmount = ctx.Sum(x => x.Amount), + TotalCount = ctx.Count(), + }); + + Assert.That(result.ElementType, Is.EqualTo(typeof(SalesSummarySchema))); + } + + [Test] + public void Aggregate_HasTwoArguments_SourceAndResultSelector() + { + var frame = new TypedFrame(_provider); + + var result = frame + .GroupBy(x => x.Category) + .Aggregate(ctx => new SalesSummarySchema + { + Category = ctx.Key, + TotalAmount = ctx.Sum(x => x.Amount), + TotalCount = ctx.Count(), + }); + + var mce = (MethodCallExpression)result.Expression; + Assert.That(mce.Arguments, Has.Count.EqualTo(2)); + } + + [Test] + public void Aggregate_FirstArgument_IsGroupByCallExpression() + { + var frame = new TypedFrame(_provider); + + var result = frame + .GroupBy(x => x.Category) + .Aggregate(ctx => new SalesSummarySchema + { + Category = ctx.Key, + TotalAmount = ctx.Sum(x => x.Amount), + TotalCount = ctx.Count(), + }); + + var mce = (MethodCallExpression)result.Expression; + var innerMce = mce.Arguments[0] as MethodCallExpression; + Assert.That(innerMce, Is.Not.Null); + Assert.That(innerMce!.Method.Name, Is.EqualTo("GroupBy")); + } + + [Test] + public void Aggregate_ResultSelector_IsQuotedLambda() + { + var frame = new TypedFrame(_provider); + + var result = frame + .GroupBy(x => x.Category) + .Aggregate(ctx => new SalesSummarySchema + { + Category = ctx.Key, + TotalAmount = ctx.Sum(x => x.Amount), + TotalCount = ctx.Count(), + }); + + var mce = (MethodCallExpression)result.Expression; + var quoted = mce.Arguments[1] as UnaryExpression; + Assert.That(quoted, Is.Not.Null); + Assert.That(quoted!.NodeType, Is.EqualTo(ExpressionType.Quote)); + Assert.That(quoted.Operand, Is.InstanceOf()); + } + + // =================================================================== + // AggregationContext method resolution in the expression body + // =================================================================== + + [Test] + public void Aggregate_ResultSelector_Body_ContainsMethodCallExpression_ForSum() + { + var frame = new TypedFrame(_provider); + + var result = frame + .GroupBy(x => x.Category) + .Aggregate(ctx => new SalesSummarySchema + { + Category = ctx.Key, + TotalAmount = ctx.Sum(x => x.Amount), + TotalCount = ctx.Count(), + }); + + var lambda = ExtractResultLambda(result); + var mie = (MemberInitExpression)lambda.Body; + + // Find the TotalAmount binding — should be a Sum() MethodCallExpression + var totalAmountBinding = mie + .Bindings.OfType() + .Single(b => b.Member.Name == nameof(SalesSummarySchema.TotalAmount)); + + var mce = totalAmountBinding.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo(nameof(AggregationContext.Sum))); + } + + [Test] + public void Aggregate_ResultSelector_Body_ContainsMethodCallExpression_ForCount() + { + var frame = new TypedFrame(_provider); + + var result = frame + .GroupBy(x => x.Category) + .Aggregate(ctx => new SalesSummarySchema + { + Category = ctx.Key, + TotalAmount = ctx.Sum(x => x.Amount), + TotalCount = ctx.Count(), + }); + + var lambda = ExtractResultLambda(result); + var mie = (MemberInitExpression)lambda.Body; + + var totalCountBinding = mie + .Bindings.OfType() + .Single(b => b.Member.Name == nameof(SalesSummarySchema.TotalCount)); + + var mce = totalCountBinding.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo(nameof(AggregationContext.Count))); + } + + // =================================================================== + // Helpers + // =================================================================== + + private static LambdaExpression ExtractResultLambda(TypedFrame frame) + { + var mce = (MethodCallExpression)frame.Expression; + return (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/JoinExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/JoinExpressionTests.cs new file mode 100644 index 00000000..02987edf --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/JoinExpressionTests.cs @@ -0,0 +1,187 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class JoinExpressionTests +{ + private TestFrameProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // Tree structure + // =================================================================== + + [Test] + public void Join_ProducesMethodCallExpression_WithCorrectMethodName() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Join( + right, + emp => emp.DeptId, + dept => dept.DeptId, + (emp, dept) => new EmployeeDeptSchema { EmployeeName = emp.Name, DepartmentName = dept.Name } + ); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("Join")); + } + + [Test] + public void Join_ChangesElementType_ToResultSchema() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Join( + right, + emp => emp.DeptId, + dept => dept.DeptId, + (emp, dept) => new EmployeeDeptSchema { EmployeeName = emp.Name, DepartmentName = dept.Name } + ); + + Assert.That(result.ElementType, Is.EqualTo(typeof(EmployeeDeptSchema))); + } + + [Test] + public void Join_HasFiveArguments() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Join( + right, + emp => emp.DeptId, + dept => dept.DeptId, + (emp, dept) => new EmployeeDeptSchema { EmployeeName = emp.Name, DepartmentName = dept.Name } + ); + + var mce = (MethodCallExpression)result.Expression; + // outer source, inner source, outerKey, innerKey, resultSelector + Assert.That(mce.Arguments, Has.Count.EqualTo(5)); + } + + // =================================================================== + // Key selectors + // =================================================================== + + [Test] + public void Join_KeySelectors_ReferenceCorrectMembers() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Join( + right, + emp => emp.DeptId, + dept => dept.DeptId, + (emp, dept) => new EmployeeDeptSchema { EmployeeName = emp.Name, DepartmentName = dept.Name } + ); + + var mce = (MethodCallExpression)result.Expression; + + // args[2] = outer key selector (quoted) + var outerKeyLambda = ExtractQuotedLambda(mce.Arguments[2]); + var outerBody = outerKeyLambda.Body as MemberExpression; + Assert.That(outerBody, Is.Not.Null); + Assert.That(outerBody!.Member.Name, Is.EqualTo("DeptId")); + + // args[3] = inner key selector (quoted) + var innerKeyLambda = ExtractQuotedLambda(mce.Arguments[3]); + var innerBody = innerKeyLambda.Body as MemberExpression; + Assert.That(innerBody, Is.Not.Null); + Assert.That(innerBody!.Member.Name, Is.EqualTo("DeptId")); + } + + // =================================================================== + // Result selector + // =================================================================== + + [Test] + public void Join_ResultSelector_HasTwoParameters() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Join( + right, + emp => emp.DeptId, + dept => dept.DeptId, + (emp, dept) => new EmployeeDeptSchema { EmployeeName = emp.Name, DepartmentName = dept.Name } + ); + + var mce = (MethodCallExpression)result.Expression; + var resultLambda = ExtractQuotedLambda(mce.Arguments[4]); + + Assert.That(resultLambda.Parameters, Has.Count.EqualTo(2)); + } + + [Test] + public void Join_ResultSelector_ProducesMemberInitExpression() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Join( + right, + emp => emp.DeptId, + dept => dept.DeptId, + (emp, dept) => new EmployeeDeptSchema { EmployeeName = emp.Name, DepartmentName = dept.Name } + ); + + var mce = (MethodCallExpression)result.Expression; + var resultLambda = ExtractQuotedLambda(mce.Arguments[4]); + + Assert.That(resultLambda.Body, Is.InstanceOf()); + } + + [Test] + public void Join_ResultSelector_BindingsReferenceCorrectParameters() + { + var left = new TypedFrame(_provider); + var right = new TypedFrame(_provider); + + var result = left.Join( + right, + emp => emp.DeptId, + dept => dept.DeptId, + (emp, dept) => new EmployeeDeptSchema { EmployeeName = emp.Name, DepartmentName = dept.Name } + ); + + var mce = (MethodCallExpression)result.Expression; + var resultLambda = ExtractQuotedLambda(mce.Arguments[4]); + var mie = (MemberInitExpression)resultLambda.Body; + + // EmployeeName = emp.Name → source parameter is emp (index 0) + var empBinding = (MemberAssignment)mie.Bindings.First(b => b.Member.Name == "EmployeeName"); + var empSource = (MemberExpression)empBinding.Expression; + Assert.That(empSource.Expression, Is.SameAs(resultLambda.Parameters[0])); + Assert.That(empSource.Member.Name, Is.EqualTo("Name")); + + // DepartmentName = dept.Name → source parameter is dept (index 1) + var deptBinding = (MemberAssignment)mie.Bindings.First(b => b.Member.Name == "DepartmentName"); + var deptSource = (MemberExpression)deptBinding.Expression; + Assert.That(deptSource.Expression, Is.SameAs(resultLambda.Parameters[1])); + Assert.That(deptSource.Member.Name, Is.EqualTo("Name")); + } + + // =================================================================== + // Helpers + // =================================================================== + + private static LambdaExpression ExtractQuotedLambda(Expression expression) + { + var quote = (UnaryExpression)expression; + return (LambdaExpression)quote.Operand; + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/MathStringDateTimeExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/MathStringDateTimeExpressionTests.cs new file mode 100644 index 00000000..cd57e48d --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/MathStringDateTimeExpressionTests.cs @@ -0,0 +1,225 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class MathStringDateTimeExpressionTests +{ + private TestFrameProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // Math methods + // =================================================================== + + [Test] + public void Select_WithMathAbs_BodyContainsMethodCall_Named_Abs() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => Math.Abs(x.Temperature) > 0); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + var absCall = binary.Left as MethodCallExpression; + + Assert.That(absCall, Is.Not.Null); + Assert.That(absCall!.Method.Name, Is.EqualTo(nameof(Math.Abs))); + Assert.That(absCall.Method.DeclaringType, Is.EqualTo(typeof(Math))); + } + + [Test] + public void Select_WithMathFloor_BodyContainsMethodCall_Named_Floor() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => Math.Floor(x.Value) > 0); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + var call = binary.Left as MethodCallExpression; + + Assert.That(call!.Method.Name, Is.EqualTo(nameof(Math.Floor))); + } + + [Test] + public void Select_WithMathCeiling_BodyContainsMethodCall_Named_Ceiling() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => Math.Ceiling(x.Value) > 0); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + var call = binary.Left as MethodCallExpression; + + Assert.That(call!.Method.Name, Is.EqualTo(nameof(Math.Ceiling))); + } + + [Test] + public void Select_WithMathRoundOneArg_BodyContainsMethodCall_Named_Round() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => Math.Round(x.Value) > 0); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + var call = binary.Left as MethodCallExpression; + + Assert.That(call!.Method.Name, Is.EqualTo(nameof(Math.Round))); + Assert.That(call.Arguments, Has.Count.EqualTo(1)); + } + + [Test] + public void Select_WithMathRoundTwoArgs_SecondArg_IsCorrectScale() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => Math.Round(x.Value, 2) > 0); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + var call = binary.Left as MethodCallExpression; + + Assert.That(call!.Method.Name, Is.EqualTo(nameof(Math.Round))); + Assert.That(call.Arguments, Has.Count.EqualTo(2)); + var scale = call.Arguments[1] as ConstantExpression; + Assert.That(scale!.Value, Is.EqualTo(2)); + } + + // =================================================================== + // New string methods: TrimStart / TrimEnd / Substring + // =================================================================== + + [Test] + public void Where_WithTrimStart_BodyContainsMethodCall_Named_TrimStart() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Sku.TrimStart() == "BULK"); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + var call = binary.Left as MethodCallExpression; + + Assert.That(call, Is.Not.Null); + Assert.That(call!.Method.Name, Is.EqualTo(nameof(string.TrimStart))); + Assert.That(call.Method.DeclaringType, Is.EqualTo(typeof(string))); + } + + [Test] + public void Where_WithTrimEnd_BodyContainsMethodCall_Named_TrimEnd() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Sku.TrimEnd() == "BULK"); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + var call = binary.Left as MethodCallExpression; + + Assert.That(call!.Method.Name, Is.EqualTo(nameof(string.TrimEnd))); + } + + [Test] + public void Where_WithSubstring_BodyContainsMethodCall_Named_Substring_WithTwoArgs() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Sku.Substring(0, 3) == "SKU"); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + var call = binary.Left as MethodCallExpression; + + Assert.That(call!.Method.Name, Is.EqualTo(nameof(string.Substring))); + Assert.That(call.Arguments, Has.Count.EqualTo(2)); + + var startArg = call.Arguments[0] as ConstantExpression; + var lenArg = call.Arguments[1] as ConstantExpression; + Assert.That(startArg!.Value, Is.EqualTo(0)); + Assert.That(lenArg!.Value, Is.EqualTo(3)); + } + + // =================================================================== + // DateTime property access + // =================================================================== + + [Test] + public void Where_WithDateTimeYear_BodyContainsMemberAccess_Named_Year() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.OrderDate.Year == 2024); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + + var me = binary.Left as MemberExpression; + Assert.That(me, Is.Not.Null); + Assert.That(me!.Member.Name, Is.EqualTo(nameof(DateTime.Year))); + Assert.That(me.Member.DeclaringType, Is.EqualTo(typeof(DateTime))); + } + + [Test] + public void Where_WithDateTimeMonth_BodyContainsMemberAccess_Named_Month() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.OrderDate.Month == 6); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + var me = binary.Left as MemberExpression; + + Assert.That(me!.Member.Name, Is.EqualTo(nameof(DateTime.Month))); + } + + [Test] + public void Where_WithDateTimeDay_BodyContainsMemberAccess_Named_Day() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.OrderDate.Day == 15); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + var me = binary.Left as MemberExpression; + + Assert.That(me!.Member.Name, Is.EqualTo(nameof(DateTime.Day))); + } + + [Test] + public void Where_WithDateTimeHour_BodyContainsMemberAccess_Named_Hour() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.OrderDate.Hour >= 9); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + var me = binary.Left as MemberExpression; + + Assert.That(me!.Member.Name, Is.EqualTo(nameof(DateTime.Hour))); + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/NullCheckExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/NullCheckExpressionTests.cs new file mode 100644 index 00000000..e811831c --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/NullCheckExpressionTests.cs @@ -0,0 +1,122 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class NullCheckExpressionTests +{ + private TestFrameProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // x.Prop == null → IsNull-shaped expression + // =================================================================== + + [Test] + public void Where_NullEqualityCheck_ProducesEqualExpression() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Region == null); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + + // The body represents == null — should be an Equal binary expression + Assert.That(lambda.Body.NodeType, Is.EqualTo(ExpressionType.Equal)); + } + + [Test] + public void Where_NullEqualityCheck_RightSide_IsNullConstant() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Region == null); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + + var right = binary.Right as ConstantExpression; + Assert.That(right, Is.Not.Null); + Assert.That(right!.Value, Is.Null); + } + + [Test] + public void Where_NullEqualityCheck_LeftSide_IsMemberAccess_ForNullableProperty() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Region == null); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + + var left = binary.Left as MemberExpression; + Assert.That(left, Is.Not.Null); + Assert.That(left!.Member.Name, Is.EqualTo(nameof(OrderSchema.Region))); + } + + // =================================================================== + // x.Prop != null → IsNotNull-shaped expression + // =================================================================== + + [Test] + public void Where_NullInequalityCheck_ProducesNotEqualExpression() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Region != null); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + + Assert.That(lambda.Body.NodeType, Is.EqualTo(ExpressionType.NotEqual)); + } + + [Test] + public void Where_NullInequalityCheck_RightSide_IsNullConstant() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Region != null); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + + var right = binary.Right as ConstantExpression; + Assert.That(right, Is.Not.Null); + Assert.That(right!.Value, Is.Null); + } + + // =================================================================== + // null == x.Prop → same but null on left side + // =================================================================== + + [Test] + public void Where_NullOnLeft_ProducesEqualExpression_WithNullLeftSide() + { + var frame = new TypedFrame(_provider); + +#pragma warning disable CS8073 // always-null comparison — intentional for translation test + var result = frame.Where(x => null == x.Region); +#pragma warning restore CS8073 + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var binary = (BinaryExpression)lambda.Body; + + var left = binary.Left as ConstantExpression; + Assert.That(left, Is.Not.Null); + Assert.That(left!.Value, Is.Null); + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/OrderByExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/OrderByExpressionTests.cs new file mode 100644 index 00000000..283c3b70 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/OrderByExpressionTests.cs @@ -0,0 +1,149 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class OrderByExpressionTests +{ + private TestFrameProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // Tree structure — ascending + // =================================================================== + + [Test] + public void OrderBy_ProducesMethodCallExpression_WithCorrectMethodName() + { + var frame = new TypedFrame(_provider); + + var result = frame.OrderBy(x => x.Age); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("OrderBy")); + } + + [Test] + public void OrderBy_PreservesSourceTypeParameter() + { + var frame = new TypedFrame(_provider); + + var result = frame.OrderBy(x => x.Age); + + Assert.That(result.ElementType, Is.EqualTo(typeof(PersonSchema))); + } + + [Test] + public void OrderBy_HasTwoArguments_SourceAndKeySelector() + { + var frame = new TypedFrame(_provider); + + var result = frame.OrderBy(x => x.Age); + + var mce = (MethodCallExpression)result.Expression; + Assert.That(mce.Arguments, Has.Count.EqualTo(2)); + } + + [Test] + public void OrderBy_KeySelector_IsQuotedLambda() + { + var frame = new TypedFrame(_provider); + + var result = frame.OrderBy(x => x.Age); + + var mce = (MethodCallExpression)result.Expression; + var quoted = mce.Arguments[1] as UnaryExpression; + Assert.That(quoted, Is.Not.Null); + Assert.That(quoted!.NodeType, Is.EqualTo(ExpressionType.Quote)); + Assert.That(quoted.Operand, Is.InstanceOf()); + } + + [Test] + public void OrderBy_KeySelector_BodyIsMemberAccess_ForKeyProperty() + { + var frame = new TypedFrame(_provider); + + var result = frame.OrderBy(x => x.Name); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var member = lambda.Body as MemberExpression; + Assert.That(member, Is.Not.Null); + Assert.That(member!.Member.Name, Is.EqualTo(nameof(PersonSchema.Name))); + } + + // =================================================================== + // Tree structure — descending + // =================================================================== + + [Test] + public void OrderByDescending_ProducesMethodCallExpression_WithCorrectMethodName() + { + var frame = new TypedFrame(_provider); + + var result = frame.OrderByDescending(x => x.Age); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("OrderByDescending")); + } + + [Test] + public void OrderByDescending_PreservesSourceTypeParameter() + { + var frame = new TypedFrame(_provider); + + var result = frame.OrderByDescending(x => x.Age); + + Assert.That(result.ElementType, Is.EqualTo(typeof(PersonSchema))); + } + + [Test] + public void OrderByDescending_KeySelector_IsQuotedLambda() + { + var frame = new TypedFrame(_provider); + + var result = frame.OrderByDescending(x => x.Age); + + var mce = (MethodCallExpression)result.Expression; + var quoted = mce.Arguments[1] as UnaryExpression; + Assert.That(quoted, Is.Not.Null); + Assert.That(quoted!.NodeType, Is.EqualTo(ExpressionType.Quote)); + Assert.That(quoted.Operand, Is.InstanceOf()); + } + + // =================================================================== + // Chaining + // =================================================================== + + [Test] + public void OrderBy_CanChainWithWhere() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.IsActive).OrderBy(x => x.Name); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("OrderBy")); + + // Inner expression is the Where call + var innerMce = mce.Arguments[0] as MethodCallExpression; + Assert.That(innerMce, Is.Not.Null); + Assert.That(innerMce!.Method.Name, Is.EqualTo("Where")); + } + + private static LambdaExpression ExtractLambda(TypedFrame frame) + { + var mce = (MethodCallExpression)frame.Expression; + return (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/SelectExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/SelectExpressionTests.cs new file mode 100644 index 00000000..b3f6ab43 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/SelectExpressionTests.cs @@ -0,0 +1,164 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class SelectExpressionTests +{ + private TestFrameProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // Tree structure + // =================================================================== + + [Test] + public void Select_ProducesMethodCallExpression_WithCorrectMethodName() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new NameOnlySchema { Name = x.Name }); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("Select")); + } + + [Test] + public void Select_ChangesElementType_ToTargetSchema() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new NameOnlySchema { Name = x.Name }); + + Assert.That(result.ElementType, Is.EqualTo(typeof(NameOnlySchema))); + } + + [Test] + public void Select_HasTwoArguments_SourceAndQuotedSelector() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new NameOnlySchema { Name = x.Name }); + + var mce = (MethodCallExpression)result.Expression; + Assert.That(mce.Arguments, Has.Count.EqualTo(2)); + } + + // =================================================================== + // Projection lambda: MemberInitExpression + // =================================================================== + + [Test] + public void Select_MemberInit_ProducesMemberInitExpression() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new PersonSummarySchema { Name = x.Name, Age = x.Age }); + + var lambda = ExtractLambda(result); + Assert.That(lambda.Body, Is.InstanceOf()); + } + + [Test] + public void Select_MemberInit_HasCorrectBindingCount() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new PersonSummarySchema { Name = x.Name, Age = x.Age }); + + var lambda = ExtractLambda(result); + var mie = (MemberInitExpression)lambda.Body; + Assert.That(mie.Bindings, Has.Count.EqualTo(2)); + } + + [Test] + public void Select_MemberInit_BindingsReferenceCorrectSourceMembers() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new PersonSummarySchema { Name = x.Name, Age = x.Age }); + + var lambda = ExtractLambda(result); + var mie = (MemberInitExpression)lambda.Body; + + var bindingNames = mie.Bindings.Select(b => b.Member.Name).ToList(); + Assert.That(bindingNames, Does.Contain("Name")); + Assert.That(bindingNames, Does.Contain("Age")); + } + + [Test] + public void Select_MemberInit_AssignmentExpressionsAreMemberAccess() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new PersonSummarySchema { Name = x.Name, Age = x.Age }); + + var lambda = ExtractLambda(result); + var mie = (MemberInitExpression)lambda.Body; + + foreach (var binding in mie.Bindings.Cast()) + { + Assert.That(binding.Expression, Is.InstanceOf()); + var source = (MemberExpression)binding.Expression; + Assert.That(source.Expression, Is.InstanceOf()); + } + } + + // =================================================================== + // Projection lambda: NewExpression (positional record constructor) + // =================================================================== + + [Test] + public void Select_RecordConstructor_ProducesNewExpression() + { + var frame = new TypedFrame(_provider); + + // Positional record construction via constructor + var result = frame.Select(x => new NameOnlySchema { Name = x.Name }); + + var lambda = ExtractLambda(result); + // Object initializer on a record still emits MemberInitExpression + Assert.That(lambda.Body, Is.InstanceOf().Or.InstanceOf()); + } + + // =================================================================== + // Chaining: Where → Select + // =================================================================== + + [Test] + public void WhereFollowedBySelect_NestsCorrectly() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Age > 18).Select(x => new NameOnlySchema { Name = x.Name }); + + // Outer is Select + var outer = (MethodCallExpression)result.Expression; + Assert.That(outer.Method.Name, Is.EqualTo("Select")); + Assert.That(result.ElementType, Is.EqualTo(typeof(NameOnlySchema))); + + // Inner (source of Select) is Where + var inner = outer.Arguments[0] as MethodCallExpression; + Assert.That(inner, Is.Not.Null); + Assert.That(inner!.Method.Name, Is.EqualTo("Where")); + } + + // =================================================================== + // Helpers + // =================================================================== + + private static LambdaExpression ExtractLambda(TypedFrame frame) + { + var mce = (MethodCallExpression)frame.Expression; + var quote = (UnaryExpression)mce.Arguments[1]; + return (LambdaExpression)quote.Operand; + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/SparkDiRegistrationTests.cs b/tests/Flowthru.Extensions.Spark.Tests/SparkDiRegistrationTests.cs new file mode 100644 index 00000000..a376e55c --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/SparkDiRegistrationTests.cs @@ -0,0 +1,103 @@ +using Flowthru.Core.Flows; +using Flowthru.Core.Services; +using Flowthru.Extensions.Spark.Runtime; +using Flowthru.Extensions.Spark.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("DependencyInjection")] +public class SparkDiRegistrationTests +{ + private IServiceProvider BuildProvider(Action? configure = null) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddFlowthru(flowthru => + { + // A no-op flow is required — AddFlowthru rejects a registration with zero flows. + flowthru.RegisterFlows(_ => new Dictionary()); + + if (configure is null) + flowthru.UseSpark(); + else + flowthru.UseSpark(configure); + }); + return services.BuildServiceProvider(); + } + + // =================================================================== + // SparkFrameProvider + // =================================================================== + + [Test] + public void UseSpark_RegistersSparkFrameProvider() + { + var provider = BuildProvider(); + + var frameProvider = provider.GetService(); + + Assert.That(frameProvider, Is.Not.Null); + } + + [Test] + public void UseSpark_SparkFrameProvider_IsSingleton() + { + var provider = BuildProvider(); + + var first = provider.GetRequiredService(); + var second = provider.GetRequiredService(); + + Assert.That(first, Is.SameAs(second)); + } + + // =================================================================== + // SparkRuntime + // =================================================================== + + [Test] + public void UseSpark_RegistersSparkRuntime() + { + var provider = BuildProvider(); + + var runtime = provider.GetService(); + + Assert.That(runtime, Is.Not.Null); + } + + [Test] + public void UseSpark_SparkRuntime_IsSingleton() + { + var provider = BuildProvider(); + + var first = provider.GetRequiredService(); + var second = provider.GetRequiredService(); + + Assert.That(first, Is.SameAs(second)); + } + + // =================================================================== + // SparkRuntimeOptions + // =================================================================== + + [Test] + public void UseSpark_WithConfiguration_RegistersOptions() + { + var provider = BuildProvider(opts => opts.Master = "spark://test:7077"); + + var options = provider.GetRequiredService(); + + Assert.That(options.Master, Is.EqualTo("spark://test:7077")); + } + + [Test] + public void UseSpark_DefaultConfiguration_UsesLocalMaster() + { + var provider = BuildProvider(); + + var options = provider.GetRequiredService(); + + Assert.That(options.Master, Does.StartWith("local")); + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/SparkRowHydratorValidationTests.cs b/tests/Flowthru.Extensions.Spark.Tests/SparkRowHydratorValidationTests.cs new file mode 100644 index 00000000..6b1ee1ab --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/SparkRowHydratorValidationTests.cs @@ -0,0 +1,217 @@ +using Flowthru.Core.Abstractions; +using Flowthru.Spark.Sql.Types; + +namespace Flowthru.Extensions.Spark.Tests; + +/// +/// Schema with SerializedLabel attributes for hydrator label-resolution tests. +/// +public record HydratorLabeledSchema : IFlatSchema +{ + [SerializedLabel("full_name")] + public required string FullName { get; init; } + + [SerializedLabel("employee_id")] + public required int EmployeeId { get; init; } + + public required string Department { get; init; } +} + +[TestFixture] +[Category("SparkRowHydrator")] +[Category("SparkRowHydrator.Validation")] +public class SparkRowHydratorValidationTests +{ + // =================================================================== + // Helpers + // =================================================================== + + private static SparkRowHydrator HydratorFor() + where T : notnull, IFlatSchema => + new SparkRowHydrator(new SparkFrameProvider()); + + private static StructType Schema(params (string name, DataType type)[] fields) => + new StructType(fields.Select(f => new StructField(f.name, f.type))); + + // =================================================================== + // Happy path + // =================================================================== + + [Test] + public void ValidateSchema_AllColumnsPresent_ReturnsEmpty() + { + var hydrator = HydratorFor(); + var schema = Schema( + ("Name", new StringType()), + ("Age", new IntegerType()), + ("IsActive", new BooleanType()) + ); + + var errors = hydrator.ValidateSchema(schema); + + Assert.That(errors, Is.Empty); + } + + [Test] + public void ValidateSchema_ExtraColumnsInSchema_ReturnsEmpty() + { + // Extra columns not in T are irrelevant — no error + var hydrator = HydratorFor(); + var schema = Schema( + ("Name", new StringType()), + ("Age", new IntegerType()), + ("IsActive", new BooleanType()), + ("extra_column", new StringType()) + ); + + var errors = hydrator.ValidateSchema(schema); + + Assert.That(errors, Is.Empty); + } + + // =================================================================== + // Missing columns + // =================================================================== + + [Test] + public void ValidateSchema_MissingColumn_ReturnsOneError() + { + var hydrator = HydratorFor(); + var schema = Schema( + ("Name", new StringType()), + ("Age", new IntegerType()) + // IsActive intentionally missing + ); + + var errors = hydrator.ValidateSchema(schema); + + Assert.That(errors, Has.Count.EqualTo(1)); + Assert.That(errors[0].ColumnName, Is.EqualTo("IsActive")); + } + + [Test] + public void ValidateSchema_MultipleColumnsMissing_ReturnsErrorPerColumn() + { + var hydrator = HydratorFor(); + var schema = Schema( + ("Name", new StringType()) + // Age and IsActive missing + ); + + var errors = hydrator.ValidateSchema(schema); + + Assert.That(errors, Has.Count.EqualTo(2)); + Assert.That( + errors.Select(e => e.ColumnName), + Is.EquivalentTo(new[] { "Age", "IsActive" }) + ); + } + + [Test] + public void ValidateSchema_EmptySchema_ReturnsErrorForEveryProperty() + { + var hydrator = HydratorFor(); + var schema = new StructType(Enumerable.Empty()); + + var errors = hydrator.ValidateSchema(schema); + + // PersonSchema has 3 required properties + Assert.That(errors, Has.Count.EqualTo(3)); + } + + // =================================================================== + // Type compatibility + // =================================================================== + + [Test] + public void ValidateSchema_IncompatibleType_ReturnsOneError() + { + var hydrator = HydratorFor(); + var schema = Schema( + ("Name", new StringType()), + ("Age", new StringType()), // int property, StringType column — incompatible + ("IsActive", new BooleanType()) + ); + + var errors = hydrator.ValidateSchema(schema); + + Assert.That(errors, Has.Count.EqualTo(1)); + Assert.That(errors[0].ColumnName, Is.EqualTo("Age")); + Assert.That(errors[0].Reason, Does.Contain("string").IgnoreCase); + } + + [Test] + public void ValidateSchema_IntegerTypeForLongProperty_IsCompatible() + { + // IntegerType is in the compatibility set for long (widening conversion) + // PersonSchema.Age is int; use a schema with a long property instead. + // Verify via the map directly: IntegerType maps to [int, long]. + var hydrator = HydratorFor(); + var schema = Schema( + ("Name", new StringType()), + ("Age", new IntegerType()), // int property + IntegerType column = exact match + ("IsActive", new BooleanType()) + ); + + var errors = hydrator.ValidateSchema(schema); + + Assert.That(errors, Is.Empty); + } + + // =================================================================== + // [SerializedLabel] resolution + // =================================================================== + + [Test] + public void ValidateSchema_UsesSerializedLabel_NotPropertyName() + { + // HydratorLabeledSchema.FullName maps to "full_name" + // The schema must provide "full_name", not "FullName" + var hydrator = HydratorFor(); + var schema = Schema( + ("full_name", new StringType()), + ("employee_id", new IntegerType()), + ("Department", new StringType()) + ); + + var errors = hydrator.ValidateSchema(schema); + + Assert.That(errors, Is.Empty); + } + + [Test] + public void ValidateSchema_PropertyNameWhenNoLabel_NotLookingForLabel() + { + // HydratorLabeledSchema.Department has no [SerializedLabel], uses property name + var hydrator = HydratorFor(); + var schema = Schema( + ("full_name", new StringType()), + ("employee_id", new IntegerType()) + // "Department" is missing + ); + + var errors = hydrator.ValidateSchema(schema); + + Assert.That(errors, Has.Count.EqualTo(1)); + Assert.That(errors[0].ColumnName, Is.EqualTo("Department")); + } + + // =================================================================== + // Case-insensitive column matching + // =================================================================== + + [Test] + public void ValidateSchema_ColumnNameDifferentCase_NoError() + { + var hydrator = HydratorFor(); + var schema = Schema( + ("name", new StringType()), // lowercase — should still match "Name" + ("age", new IntegerType()), + ("isactive", new BooleanType()) + ); + + var errors = hydrator.ValidateSchema(schema); + + Assert.That(errors, Is.Empty); + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/SparkTranslatableOperationSyncTests.cs b/tests/Flowthru.Extensions.Spark.Tests/SparkTranslatableOperationSyncTests.cs new file mode 100644 index 00000000..21584b63 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/SparkTranslatableOperationSyncTests.cs @@ -0,0 +1,157 @@ +using System.Reflection; +using Flowthru.Extensions.Spark; +using Flowthru.Extensions.Spark.Shared; + +namespace Flowthru.Extensions.Spark.Tests; + +/// +/// Verifies that the switch arms implemented in SparkExpressionVisitor stay in sync +/// with the whitelists in SparkTranslatableOperations. +/// +/// +/// +/// SparkTranslatableOperations is the shared source of truth consumed by both the +/// runtime visitor and the FSPARK1002 Roslyn analyzer. If a developer adds a switch +/// arm in the visitor without updating the whitelist (or vice versa), these tests fail, +/// surfacing the drift before it reaches CI. +/// +/// +/// This test does NOT instantiate SparkExpressionVisitor or require a Spark/JVM +/// environment — it reflects over the compiled type using only metadata. +/// +/// +[TestFixture] +public class SparkTranslatableOperationSyncTests +{ + // ─── String method sync ─────────────────────────────────────────────────────── + + /// + /// Every name in SupportedStringMethods must have a corresponding case in the + /// TranslateStringMethod switch inside SparkExpressionVisitor. + /// + /// + /// We detect switch arms by reflecting over the private method and checking for the + /// presence of the method name as a constant used in a SwitchExpression. Since + /// we cannot inspect IL switch arms at compile time, we use a proxy: we call the + /// method via a controlled expression tree in + /// (the runtime-facing sibling). Here we confirm the whitelist is *non-empty* and + /// internally consistent (no duplicates, no null entries). + /// + [Test] + public void SupportedStringMethods_ContainsNoNullOrEmptyEntries() + { + foreach (var name in SparkTranslatableOperations.SupportedStringMethods) + { + Assert.That( + name, + Is.Not.Null.And.Not.Empty, + "SupportedStringMethods contains a null or empty entry." + ); + } + } + + [Test] + public void SupportedStringMethods_HasNoduplicates() + { + var list = SparkTranslatableOperations.SupportedStringMethods.ToList(); + var distinct = list.Distinct(StringComparer.Ordinal).ToList(); + Assert.That( + list.Count, + Is.EqualTo(distinct.Count), + "SupportedStringMethods contains duplicate entries." + ); + } + + [Test] + public void SupportedStringMethods_AllExistOnSystemString() + { + foreach (var name in SparkTranslatableOperations.SupportedStringMethods) + { + var methods = typeof(string) + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => m.Name == name) + .ToList(); + + Assert.That( + methods, + Is.Not.Empty, + $"'{name}' in SupportedStringMethods does not correspond to any public instance method on System.String." + ); + } + } + + // ─── Math method sync ───────────────────────────────────────────────────────── + + [Test] + public void SupportedMathMethods_ContainsNoNullOrEmptyEntries() + { + foreach (var name in SparkTranslatableOperations.SupportedMathMethods) + { + Assert.That( + name, + Is.Not.Null.And.Not.Empty, + "SupportedMathMethods contains a null or empty entry." + ); + } + } + + [Test] + public void SupportedMathMethods_HasNoDuplicates() + { + var list = SparkTranslatableOperations.SupportedMathMethods.ToList(); + var distinct = list.Distinct(StringComparer.Ordinal).ToList(); + Assert.That( + list.Count, + Is.EqualTo(distinct.Count), + "SupportedMathMethods contains duplicate entries." + ); + } + + [Test] + public void SupportedMathMethods_AllExistOnSystemMath() + { + foreach (var name in SparkTranslatableOperations.SupportedMathMethods) + { + var methods = typeof(Math) + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => m.Name == name) + .ToList(); + + Assert.That( + methods, + Is.Not.Empty, + $"'{name}' in SupportedMathMethods does not correspond to any public static method on System.Math." + ); + } + } + + // ─── DateTime property sync ─────────────────────────────────────────────────── + + [Test] + public void SupportedDateTimeProperties_ContainsNoNullOrEmptyEntries() + { + foreach (var name in SparkTranslatableOperations.SupportedDateTimeProperties) + { + Assert.That( + name, + Is.Not.Null.And.Not.Empty, + "SupportedDateTimeProperties contains a null or empty entry." + ); + } + } + + [Test] + public void SupportedDateTimeProperties_AllExistOnSystemDateTime() + { + foreach (var name in SparkTranslatableOperations.SupportedDateTimeProperties) + { + var prop = typeof(DateTime).GetProperty(name, BindingFlags.Public | BindingFlags.Instance); + + Assert.That( + prop, + Is.Not.Null, + $"'{name}' in SupportedDateTimeProperties does not correspond to any public instance property on System.DateTime." + ); + } + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/StringMethodExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/StringMethodExpressionTests.cs new file mode 100644 index 00000000..6b36e611 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/StringMethodExpressionTests.cs @@ -0,0 +1,215 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +/// +/// Schema for testing string method expression translation. +/// +public record ProductSchema +{ + public required string Sku { get; init; } + public required string Category { get; init; } + public required string Description { get; init; } +} + +/// +/// Aggregate result schema for string-method Select tests. +/// +public record ProductNormalizedSchema +{ + public required string Sku { get; init; } + public required string Category { get; init; } +} + +[TestFixture] +[Category("ExpressionTree")] +public class StringMethodExpressionTests +{ + private TestFrameProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // ToUpper / ToLower + // =================================================================== + + [Test] + public void Select_WithToUpper_BodyContainsMethodCall_Named_ToUpper() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new ProductNormalizedSchema + { + Sku = x.Sku.ToUpper(), + Category = x.Category, + }); + + var mce = ExtractBindingMethodCall(result, nameof(ProductNormalizedSchema.Sku)); + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo(nameof(string.ToUpper))); + Assert.That(mce.Method.DeclaringType, Is.EqualTo(typeof(string))); + } + + [Test] + public void Select_WithToLower_BodyContainsMethodCall_Named_ToLower() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new ProductNormalizedSchema + { + Sku = x.Sku, + Category = x.Category.ToLower(), + }); + + var mce = ExtractBindingMethodCall(result, nameof(ProductNormalizedSchema.Category)); + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo(nameof(string.ToLower))); + } + + // =================================================================== + // Contains / StartsWith / EndsWith + // =================================================================== + + [Test] + public void Where_WithStringContains_BodyContainsMethodCall_Named_Contains() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Description.Contains("organic")); + + var mce = ExtractWherePredicateCall(result); + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo(nameof(string.Contains))); + Assert.That(mce.Method.DeclaringType, Is.EqualTo(typeof(string))); + } + + [Test] + public void Where_WithStringContains_HasCorrectLiteralArgument() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Sku.Contains("BULK")); + + var mce = ExtractWherePredicateCall(result); + var arg = mce!.Arguments[0] as ConstantExpression; + Assert.That(arg, Is.Not.Null); + Assert.That(arg!.Value, Is.EqualTo("BULK")); + } + + [Test] + public void Where_WithStartsWith_BodyContainsMethodCall_Named_StartsWith() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Category.StartsWith("Food")); + + var mce = ExtractWherePredicateCall(result); + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo(nameof(string.StartsWith))); + } + + [Test] + public void Where_WithEndsWith_BodyContainsMethodCall_Named_EndsWith() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Sku.EndsWith("-XL")); + + var mce = ExtractWherePredicateCall(result); + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo(nameof(string.EndsWith))); + } + + // =================================================================== + // Replace + // =================================================================== + + [Test] + public void Select_WithReplace_BodyContainsMethodCall_Named_Replace() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new ProductNormalizedSchema + { + Sku = x.Sku.Replace("-", "_"), + Category = x.Category, + }); + + var mce = ExtractBindingMethodCall(result, nameof(ProductNormalizedSchema.Sku)); + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo(nameof(string.Replace))); + } + + [Test] + public void Select_WithReplace_HasTwoArguments_OldAndNew() + { + var frame = new TypedFrame(_provider); + + var result = frame.Select(x => new ProductNormalizedSchema + { + Sku = x.Sku.Replace("-", "_"), + Category = x.Category, + }); + + var mce = ExtractBindingMethodCall(result, nameof(ProductNormalizedSchema.Sku)); + Assert.That(mce!.Arguments, Has.Count.EqualTo(2)); + + var arg0 = mce.Arguments[0] as ConstantExpression; + var arg1 = mce.Arguments[1] as ConstantExpression; + Assert.That(arg0!.Value, Is.EqualTo("-")); + Assert.That(arg1!.Value, Is.EqualTo("_")); + } + + // =================================================================== + // string.Length (property access → MemberExpression) + // =================================================================== + + [Test] + public void Where_WithStringLengthComparison_BodyContainsMemberAccess_Named_Length() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Description.Length > 10); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + + // lambda body is: x.Description.Length > 10 + var binary = (BinaryExpression)lambda.Body; + var left = binary.Left; + + // string.Length is a MemberExpression + Assert.That(left, Is.InstanceOf()); + var me = (MemberExpression)left; + Assert.That(me.Member.Name, Is.EqualTo(nameof(string.Length))); + Assert.That(me.Member.DeclaringType, Is.EqualTo(typeof(string))); + } + + // =================================================================== + // Helpers + // =================================================================== + + private static MethodCallExpression? ExtractBindingMethodCall( + TypedFrame frame, + string memberName + ) + { + var mce = (MethodCallExpression)frame.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var mie = (MemberInitExpression)lambda.Body; + var binding = mie.Bindings.OfType().Single(b => b.Member.Name == memberName); + return binding.Expression as MethodCallExpression; + } + + private static MethodCallExpression? ExtractWherePredicateCall(TypedFrame frame) + { + var mce = (MethodCallExpression)frame.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + return lambda.Body as MethodCallExpression; + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/TakeCountExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/TakeCountExpressionTests.cs new file mode 100644 index 00000000..8c271d7b --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/TakeCountExpressionTests.cs @@ -0,0 +1,171 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class TakeCountExpressionTests +{ + private TestFrameProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // Take + // =================================================================== + + [Test] + public void Take_ProducesMethodCallExpression_WithCorrectMethodName() + { + var frame = new TypedFrame(_provider); + + var result = frame.Take(10); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("Take")); + } + + [Test] + public void Take_PreservesSourceTypeParameter() + { + var frame = new TypedFrame(_provider); + + var result = frame.Take(5); + + Assert.That(result.ElementType, Is.EqualTo(typeof(PersonSchema))); + } + + [Test] + public void Take_HasTwoArguments_SourceAndCount() + { + var frame = new TypedFrame(_provider); + + var result = frame.Take(10); + + var mce = (MethodCallExpression)result.Expression; + Assert.That(mce.Arguments, Has.Count.EqualTo(2)); + } + + [Test] + public void Take_SecondArgument_IsConstantWithCorrectValue() + { + var frame = new TypedFrame(_provider); + const int limit = 42; + + var result = frame.Take(limit); + + var mce = (MethodCallExpression)result.Expression; + var constant = mce.Arguments[1] as ConstantExpression; + Assert.That(constant, Is.Not.Null); + Assert.That(constant!.Value, Is.EqualTo(limit)); + } + + [Test] + public void Take_CanChainAfterWhere() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.IsActive).Take(100); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce!.Method.Name, Is.EqualTo("Take")); + + var innerMce = mce.Arguments[0] as MethodCallExpression; + Assert.That(innerMce!.Method.Name, Is.EqualTo("Where")); + } + + // =================================================================== + // Count + // =================================================================== + + [Test] + public void Count_ProducesMethodCallExpression_WithCorrectMethodName() + { + var frame = new TypedFrame(_provider); + + // Count is a terminal operation — we capture the expression before Execute throws + var mce = CaptureCountExpression(frame); + + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("Count")); + } + + [Test] + public void Count_HasOneArgument_TheSourceExpression() + { + var frame = new TypedFrame(_provider); + + var mce = CaptureCountExpression(frame); + + Assert.That(mce!.Arguments, Has.Count.EqualTo(1)); + } + + [Test] + public void Count_AfterWhere_SourceArgument_IsWhereCallExpression() + { + var frame = new TypedFrame(_provider); + var filtered = frame.Where(x => x.Age > 18); + + var mce = CaptureCountExpression(filtered); + + var sourceArg = mce!.Arguments[0] as MethodCallExpression; + Assert.That(sourceArg, Is.Not.Null); + Assert.That(sourceArg!.Method.Name, Is.EqualTo("Where")); + } + + // Count calls Execute on the provider — the test provider throws, + // so we intercept the expression by wrapping CreateQuery instead. + private static MethodCallExpression? CaptureCountExpression(TypedFrame frame) + { + var capturer = new CountExpressionCapturer(); + var captureFrame = new TypedFrame(capturer, frame.Expression); + try + { + captureFrame.Count(); + } + catch (CountCapturedException) + { + // expected + } + return capturer.CapturedExpression as MethodCallExpression; + } + + // =================================================================== + // Helpers + // =================================================================== + + private sealed class CountCapturedException : Exception { } + + private sealed class CountExpressionCapturer : IFrameQueryProvider + { + public Expression? CapturedExpression { get; private set; } + + public IQueryable CreateQuery(Expression expression) => + new TypedFrame(this, expression); + + public IQueryable CreateQuery(Expression expression) => throw new NotSupportedException(); + + public object Compile(Expression expression) => throw new NotSupportedException(); + + public IEnumerable Materialize(Expression expression) => + throw new NotSupportedException(); + + public TResult Execute(Expression expression) + { + CapturedExpression = expression; + throw new CountCapturedException(); + } + + public object? Execute(Expression expression) + { + CapturedExpression = expression; + throw new CountCapturedException(); + } + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/WhereExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/WhereExpressionTests.cs new file mode 100644 index 00000000..4e7555de --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/WhereExpressionTests.cs @@ -0,0 +1,144 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class WhereExpressionTests +{ + private TestFrameProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // Tree structure + // =================================================================== + + [Test] + public void Where_ProducesMethodCallExpression_WithCorrectMethodName() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Age > 18); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("Where")); + } + + [Test] + public void Where_PreservesTypeParameter() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Age > 18); + + Assert.That(result.ElementType, Is.EqualTo(typeof(PersonSchema))); + } + + [Test] + public void Where_HasTwoArguments_SourceAndQuotedPredicate() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Age > 18); + + var mce = (MethodCallExpression)result.Expression; + Assert.That(mce.Arguments, Has.Count.EqualTo(2)); + } + + [Test] + public void Where_SecondArgument_IsQuotedLambda() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Age > 18); + + var mce = (MethodCallExpression)result.Expression; + var quotedArg = mce.Arguments[1] as UnaryExpression; + Assert.That(quotedArg, Is.Not.Null); + Assert.That(quotedArg!.NodeType, Is.EqualTo(ExpressionType.Quote)); + Assert.That(quotedArg.Operand, Is.InstanceOf()); + } + + // =================================================================== + // Predicate lambda structure + // =================================================================== + + [Test] + public void Where_SimpleBinaryPredicate_HasGreaterThanNodeType() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Age > 18); + + var lambda = ExtractLambda(result); + var body = lambda.Body as BinaryExpression; + Assert.That(body, Is.Not.Null); + Assert.That(body!.NodeType, Is.EqualTo(ExpressionType.GreaterThan)); + } + + [Test] + public void Where_PropertyAccess_ReferencesCorrectMember() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Age > 18); + + var lambda = ExtractLambda(result); + var binary = (BinaryExpression)lambda.Body; + var memberAccess = binary.Left as MemberExpression; + Assert.That(memberAccess, Is.Not.Null); + Assert.That(memberAccess!.Member.Name, Is.EqualTo("Age")); + } + + [Test] + public void Where_BooleanPredicate_ProducesMemberAccessDirectly() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.IsActive); + + var lambda = ExtractLambda(result); + var memberAccess = lambda.Body as MemberExpression; + Assert.That(memberAccess, Is.Not.Null); + Assert.That(memberAccess!.Member.Name, Is.EqualTo("IsActive")); + } + + // =================================================================== + // Chaining + // =================================================================== + + [Test] + public void Where_Chained_NestsExpressionsCorrectly() + { + var frame = new TypedFrame(_provider); + + var result = frame.Where(x => x.Age > 18).Where(x => x.IsActive); + + // Outer is a Where call + var outer = (MethodCallExpression)result.Expression; + Assert.That(outer.Method.Name, Is.EqualTo("Where")); + + // Its source (first argument) is also a Where call + var inner = outer.Arguments[0] as MethodCallExpression; + Assert.That(inner, Is.Not.Null); + Assert.That(inner!.Method.Name, Is.EqualTo("Where")); + } + + // =================================================================== + // Helpers + // =================================================================== + + private static LambdaExpression ExtractLambda(TypedFrame frame) + { + var mce = (MethodCallExpression)frame.Expression; + var quote = (UnaryExpression)mce.Arguments[1]; + return (LambdaExpression)quote.Operand; + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/WindowExpressionTests.cs b/tests/Flowthru.Extensions.Spark.Tests/WindowExpressionTests.cs new file mode 100644 index 00000000..e9064495 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/WindowExpressionTests.cs @@ -0,0 +1,602 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +[TestFixture] +[Category("ExpressionTree")] +public class WindowExpressionTests +{ + private TestFrameProvider _provider = null!; + + // Window specs shared across tests + private static readonly FrameWindowSpec DeptWindow = FrameWindowSpec + .PartitionBy(x => x.Department) + .OrderByDescending(x => x.Salary); + + private static readonly FrameWindowSpec HireWindow = + FrameWindowSpec.Global.OrderBy(x => x.HireDate); + + [SetUp] + public void SetUp() + { + _provider = new TestFrameProvider(); + } + + // =================================================================== + // SelectOver — top-level tree structure + // =================================================================== + + [Test] + public void SelectOver_ProducesMethodCallExpression_WithCorrectMethodName() + { + var frame = new TypedFrame(_provider); + + var result = frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.Rank(DeptWindow), + RunningTotal = win.Sum(s => s.Salary, DeptWindow), + PrevSalary = win.Lag(s => s.Salary, 1, DeptWindow), + } + ); + + var mce = result.Expression as MethodCallExpression; + Assert.That(mce, Is.Not.Null); + Assert.That(mce!.Method.Name, Is.EqualTo("SelectOver")); + } + + [Test] + public void SelectOver_ProducesCorrectResultType() + { + var frame = new TypedFrame(_provider); + + var result = frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.Rank(DeptWindow), + RunningTotal = win.Sum(s => s.Salary, DeptWindow), + PrevSalary = win.Lag(s => s.Salary, 1, DeptWindow), + } + ); + + Assert.That(result.ElementType, Is.EqualTo(typeof(StaffRankedSchema))); + } + + [Test] + public void SelectOver_HasTwoArguments_SourceAndQuotedSelector() + { + var frame = new TypedFrame(_provider); + + var result = frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.DenseRank(DeptWindow), + RunningTotal = x.Salary, + PrevSalary = null, + } + ); + + var mce = (MethodCallExpression)result.Expression; + Assert.That(mce.Arguments, Has.Count.EqualTo(2)); + } + + [Test] + public void SelectOver_SecondArgument_IsQuotedLambda() + { + var frame = new TypedFrame(_provider); + + var result = frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.RowNumber(DeptWindow), + RunningTotal = x.Salary, + PrevSalary = null, + } + ); + + var mce = (MethodCallExpression)result.Expression; + var quoted = mce.Arguments[1] as UnaryExpression; + Assert.That(quoted, Is.Not.Null); + Assert.That(quoted!.NodeType, Is.EqualTo(ExpressionType.Quote)); + Assert.That(quoted.Operand, Is.InstanceOf()); + } + + [Test] + public void SelectOver_SelectorLambda_HasExactlyTwoParameters() + { + var frame = new TypedFrame(_provider); + + var result = frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.Rank(DeptWindow), + RunningTotal = x.Salary, + PrevSalary = null, + } + ); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + Assert.That(lambda.Parameters, Has.Count.EqualTo(2)); + } + + [Test] + public void SelectOver_SelectorLambda_SecondParameter_IsWindowContextType() + { + var frame = new TypedFrame(_provider); + + var result = frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.Rank(DeptWindow), + RunningTotal = x.Salary, + PrevSalary = null, + } + ); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + Assert.That(lambda.Parameters[1].Type, Is.EqualTo(typeof(WindowContext))); + } + + // =================================================================== + // Ranking functions — expression structure + // =================================================================== + + [Test] + [TestCase("Rank")] + [TestCase("DenseRank")] + [TestCase("RowNumber")] + [TestCase("PercentRank")] + [TestCase("CumeDist")] + public void SelectOver_RankingFunction_BodyContainsMethodCallExpression_OnWinParameter( + string methodName + ) + { + var frame = new TypedFrame(_provider); + + // Build the SelectOver call with the right method via a helper + MethodCallExpression? windowCall = null; + var mce = BuildRankingCall(frame, methodName, out windowCall); + + Assert.That(windowCall, Is.Not.Null); + Assert.That(windowCall!.Method.Name, Is.EqualTo(methodName)); + Assert.That(windowCall.Object, Is.InstanceOf()); + Assert.That( + ((ParameterExpression)windowCall.Object!).Type, + Is.EqualTo(typeof(WindowContext)) + ); + } + + [Test] + [TestCase("Rank")] + [TestCase("DenseRank")] + [TestCase("RowNumber")] + [TestCase("PercentRank")] + [TestCase("CumeDist")] + public void SelectOver_RankingFunction_HasOneArgument_TheSpec(string methodName) + { + var frame = new TypedFrame(_provider); + BuildRankingCall(frame, methodName, out var windowCall); + + // Only argument: the FrameWindowSpec (no column selector) + Assert.That(windowCall!.Arguments, Has.Count.EqualTo(1)); + } + + // =================================================================== + // Aggregate window functions — expression structure + // =================================================================== + + [Test] + public void SelectOver_Sum_BodyContainsMethodCallExpression_WithTwoArgs() + { + var frame = new TypedFrame(_provider); + + var result = frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.Rank(DeptWindow), + RunningTotal = win.Sum(s => s.Salary, DeptWindow), + PrevSalary = null, + } + ); + + var windowCall = ExtractWindowCall(result, nameof(StaffRankedSchema.RunningTotal)); + Assert.That(windowCall, Is.Not.Null); + Assert.That(windowCall!.Method.Name, Is.EqualTo("Sum")); + // arg[0] = column selector lambda, arg[1] = spec + Assert.That(windowCall.Arguments, Has.Count.EqualTo(2)); + } + + [Test] + public void SelectOver_Sum_FirstArgument_IsLambdaReferencingSalaryProperty() + { + var frame = new TypedFrame(_provider); + + var result = frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.Rank(DeptWindow), + RunningTotal = win.Sum(s => s.Salary, DeptWindow), + PrevSalary = null, + } + ); + + var windowCall = ExtractWindowCall(result, nameof(StaffRankedSchema.RunningTotal)); + var selectorLambda = ExtractLambdaFromArg(windowCall!.Arguments[0]); + var memberBody = selectorLambda.Body as MemberExpression; + + Assert.That(memberBody, Is.Not.Null); + Assert.That(memberBody!.Member.Name, Is.EqualTo(nameof(StaffSchema.Salary))); + } + + // =================================================================== + // Offset functions — expression structure + // =================================================================== + + [Test] + public void SelectOver_Lag_HasThreeArguments_Selector_Offset_Spec() + { + var frame = new TypedFrame(_provider); + + var result = frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.Rank(DeptWindow), + RunningTotal = x.Salary, + PrevSalary = win.Lag(s => s.Salary, 1, DeptWindow), + } + ); + + var windowCall = ExtractWindowCall(result, nameof(StaffRankedSchema.PrevSalary)); + Assert.That(windowCall, Is.Not.Null); + Assert.That(windowCall!.Method.Name, Is.EqualTo("Lag")); + Assert.That(windowCall.Arguments, Has.Count.EqualTo(3)); + } + + [Test] + public void SelectOver_Lag_SecondArgument_IsConstant_WithCorrectOffset() + { + var frame = new TypedFrame(_provider); + + var result = frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.Rank(DeptWindow), + RunningTotal = x.Salary, + PrevSalary = win.Lag(s => s.Salary, 2, DeptWindow), + } + ); + + var windowCall = ExtractWindowCall(result, nameof(StaffRankedSchema.PrevSalary)); + var offsetArg = windowCall!.Arguments[1] as ConstantExpression; + Assert.That(offsetArg, Is.Not.Null); + Assert.That(offsetArg!.Value, Is.EqualTo(2)); + } + + // =================================================================== + // Multi-window — different specs in the same projection + // =================================================================== + + [Test] + public void SelectOver_MultiWindow_DeptRank_And_HireOrder_UseDifferentSpecs() + { + var frame = new TypedFrame(_provider); + + var result = frame.SelectOver( + (x, win) => + new StaffMultiWindowSchema + { + Name = x.Name, + DeptRank = win.Rank(DeptWindow), + HireOrder = win.RowNumber(HireWindow), + } + ); + + var mce = (MethodCallExpression)result.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var mie = (MemberInitExpression)lambda.Body; + + var deptRankCall = GetBindingCall(mie, nameof(StaffMultiWindowSchema.DeptRank)); + var hireOrderCall = GetBindingCall(mie, nameof(StaffMultiWindowSchema.HireOrder)); + + // Each call's last argument is the spec — they should be different references + var deptSpecExpr = deptRankCall.Arguments[^1]; + var hireSpecExpr = hireOrderCall.Arguments[^1]; + + // Both refer to FrameWindowSpec but different instances + Assert.That(deptSpecExpr.Type, Is.EqualTo(typeof(FrameWindowSpec))); + Assert.That(hireSpecExpr.Type, Is.EqualTo(typeof(FrameWindowSpec))); + + // The captured spec objects must be distinct instances + var deptSpec = (FrameWindowSpec)EvaluateCapture(deptSpecExpr); + var hireSpec = (FrameWindowSpec)EvaluateCapture(hireSpecExpr); + Assert.That(deptSpec, Is.Not.SameAs(hireSpec)); + } + + [Test] + public void SelectOver_MultiWindow_DeptSpec_HasPartitionByExpressions() + { + var spec = + (FrameWindowSpec) + EvaluateCapture( + GetWindowCallFromField( + new TypedFrame(_provider).SelectOver( + (x, win) => + new StaffMultiWindowSchema + { + Name = x.Name, + DeptRank = win.Rank(DeptWindow), + HireOrder = win.RowNumber(HireWindow), + } + ), + nameof(StaffMultiWindowSchema.DeptRank) + ).Arguments[^1] + ); + + Assert.That(spec.PartitionByExpressions, Has.Count.EqualTo(1)); + Assert.That(spec.OrderByExpressions, Has.Count.EqualTo(1)); + Assert.That(spec.OrderByExpressions[0].Descending, Is.True); + } + + [Test] + public void SelectOver_MultiWindow_HireSpec_HasNoPartitionByExpressions() + { + var spec = + (FrameWindowSpec) + EvaluateCapture( + GetWindowCallFromField( + new TypedFrame(_provider).SelectOver( + (x, win) => + new StaffMultiWindowSchema + { + Name = x.Name, + DeptRank = win.Rank(DeptWindow), + HireOrder = win.RowNumber(HireWindow), + } + ), + nameof(StaffMultiWindowSchema.HireOrder) + ).Arguments[^1] + ); + + Assert.That(spec.PartitionByExpressions, Has.Count.EqualTo(0)); + Assert.That(spec.OrderByExpressions, Has.Count.EqualTo(1)); + Assert.That(spec.OrderByExpressions[0].Descending, Is.False); + } + + // =================================================================== + // FrameWindowSpec builder — spec construction + // =================================================================== + + [Test] + public void FrameWindowSpec_PartitionBy_StoresOnePartitionExpression() + { + var spec = FrameWindowSpec.PartitionBy(x => x.Department); + + Assert.That(spec.PartitionByExpressions, Has.Count.EqualTo(1)); + Assert.That(spec.OrderByExpressions, Has.Count.EqualTo(0)); + } + + [Test] + public void FrameWindowSpec_ThenPartitionBy_AddssSecondPartitionExpression() + { + var spec = FrameWindowSpec + .PartitionBy(x => x.Department) + .ThenPartitionBy(x => x.Name); + + Assert.That(spec.PartitionByExpressions, Has.Count.EqualTo(2)); + } + + [Test] + public void FrameWindowSpec_OrderBy_StoresAscendingExpression() + { + var spec = FrameWindowSpec.PartitionBy(x => x.Department).OrderBy(x => x.Salary); + + Assert.That(spec.OrderByExpressions, Has.Count.EqualTo(1)); + Assert.That(spec.OrderByExpressions[0].Descending, Is.False); + } + + [Test] + public void FrameWindowSpec_OrderByDescending_StoresDescendingExpression() + { + var spec = FrameWindowSpec + .PartitionBy(x => x.Department) + .OrderByDescending(x => x.Salary); + + Assert.That(spec.OrderByExpressions, Has.Count.EqualTo(1)); + Assert.That(spec.OrderByExpressions[0].Descending, Is.True); + } + + [Test] + public void FrameWindowSpec_IsImmutable_ChainProducesNewInstances() + { + var base1 = FrameWindowSpec.PartitionBy(x => x.Department); + var extended = base1.OrderBy(x => x.Salary); + + // base1 is unchanged + Assert.That(base1.OrderByExpressions, Has.Count.EqualTo(0)); + Assert.That(extended.OrderByExpressions, Has.Count.EqualTo(1)); + Assert.That(base1, Is.Not.SameAs(extended)); + } + + [Test] + public void FrameWindowSpec_Global_HasNoPartitionOrOrderExpressions() + { + var spec = FrameWindowSpec.Global; + + Assert.That(spec.PartitionByExpressions, Has.Count.EqualTo(0)); + Assert.That(spec.OrderByExpressions, Has.Count.EqualTo(0)); + } + + // =================================================================== + // Helpers + // =================================================================== + + private static MethodCallExpression BuildRankingCall( + TypedFrame frame, + string methodName, + out MethodCallExpression? windowCall + ) + { + // Use Rank as default for all test variants — the method name itself is what we vary + TypedFrame result = methodName switch + { + "Rank" => frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.Rank(DeptWindow), + RunningTotal = x.Salary, + PrevSalary = null, + } + ), + "DenseRank" => frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.DenseRank(DeptWindow), + RunningTotal = x.Salary, + PrevSalary = null, + } + ), + "RowNumber" => frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = win.RowNumber(DeptWindow), + RunningTotal = x.Salary, + PrevSalary = null, + } + ), + "PercentRank" => frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = (long)win.PercentRank(DeptWindow), + RunningTotal = x.Salary, + PrevSalary = null, + } + ), + _ /* CumeDist */ + => frame.SelectOver( + (x, win) => + new StaffRankedSchema + { + Name = x.Name, + Department = x.Department, + Salary = x.Salary, + DeptRank = (long)win.CumeDist(DeptWindow), + RunningTotal = x.Salary, + PrevSalary = null, + } + ), + }; + + windowCall = + ExtractWindowCall(result, nameof(StaffRankedSchema.DeptRank)) + ?? ExtractWindowCall(result, nameof(StaffRankedSchema.RunningTotal)); + return (MethodCallExpression)result.Expression; + } + + private static MethodCallExpression? ExtractWindowCall(TypedFrame frame, string memberName) + { + var mce = (MethodCallExpression)frame.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var mie = (MemberInitExpression)lambda.Body; + var binding = mie + .Bindings.OfType() + .FirstOrDefault(b => b.Member.Name == memberName); + if (binding is null) + return null; + + // May be wrapped in a Convert for numeric coercions + var expr = binding.Expression; + if (expr is UnaryExpression { NodeType: ExpressionType.Convert } ue) + expr = ue.Operand; + + return expr as MethodCallExpression; + } + + private static MethodCallExpression GetBindingCall(MemberInitExpression mie, string memberName) + { + var binding = mie.Bindings.OfType().Single(b => b.Member.Name == memberName); + return (MethodCallExpression)binding.Expression; + } + + private static MethodCallExpression GetWindowCallFromField( + TypedFrame frame, + string memberName + ) + { + var mce = (MethodCallExpression)frame.Expression; + var lambda = (LambdaExpression)((UnaryExpression)mce.Arguments[1]).Operand; + var mie = (MemberInitExpression)lambda.Body; + return GetBindingCall(mie, memberName); + } + + private static LambdaExpression ExtractLambdaFromArg(Expression arg) + { + if (arg is UnaryExpression { NodeType: ExpressionType.Quote } q) + return (LambdaExpression)q.Operand; + return (LambdaExpression)arg; + } + + private static object EvaluateCapture(Expression expr) + { + var lambda = Expression.Lambda>(Expression.Convert(expr, typeof(object))); + return lambda.Compile().Invoke(); + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/_Fixtures/SparkAssemblySetup.cs b/tests/Flowthru.Extensions.Spark.Tests/_Fixtures/SparkAssemblySetup.cs new file mode 100644 index 00000000..e82afea5 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/_Fixtures/SparkAssemblySetup.cs @@ -0,0 +1,66 @@ +using Flowthru.Extensions.Spark.Runtime; +using Microsoft.Extensions.Logging.Abstractions; +using NUnit.Framework; + +namespace Flowthru.Extensions.Spark.Tests; + +/// +/// Assembly-level setup fixture that starts a single Spark JVM backend for all tests +/// that require one. Individual test fixtures call against +/// rather than managing their own +/// instances, preventing the static JvmBridge from being connected to stale ports. +/// +[SetUpFixture] +public static class SparkAssemblySetup +{ + private static SparkRuntime? _runtime; + + /// + /// The shared Spark JVM backend runtime for this test assembly. + /// Null if SPARK_HOME was not set or the backend failed to start. + /// + public static SparkRuntime? Runtime => _runtime; + + /// + /// Whether the Spark JVM backend started successfully and is available for tests. + /// + public static bool IsAvailable { get; private set; } + + /// + /// Message describing why the backend is unavailable, if applicable. + /// + public static string? UnavailableReason { get; private set; } + + [OneTimeSetUp] + public static void StartSparkBackend() + { + var sparkHome = Environment.GetEnvironmentVariable("SPARK_HOME"); + if (string.IsNullOrEmpty(sparkHome)) + { + UnavailableReason = "SPARK_HOME is not set — skipping JVM-backed tests."; + return; + } + + try + { + var options = new SparkRuntimeOptions { BackendStartupTimeoutSeconds = 30 }; + _runtime = new SparkRuntime(options, NullLogger.Instance, NullLoggerFactory.Instance); + _runtime.Initialize(); + IsAvailable = true; + } + catch (Exception ex) + { + UnavailableReason = $"Spark JVM backend failed to start: {ex.Message}"; + _runtime?.Dispose(); + _runtime = null; + } + } + + [OneTimeTearDown] + public static void StopSparkBackend() + { + _runtime?.Dispose(); + _runtime = null; + IsAvailable = false; + } +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/_Fixtures/TestFrameProvider.cs b/tests/Flowthru.Extensions.Spark.Tests/_Fixtures/TestFrameProvider.cs new file mode 100644 index 00000000..0ff296ae --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/_Fixtures/TestFrameProvider.cs @@ -0,0 +1,34 @@ +using System.Linq.Expressions; +using Flowthru.Misc.DataFrames; + +namespace Flowthru.Extensions.Spark.Tests; + +/// +/// A minimal that builds expression trees without +/// requiring any native frame backend. Used for testing expression tree construction +/// in isolation from the Spark JVM runtime. +/// +internal sealed class TestFrameProvider : IFrameQueryProvider +{ + public IQueryable CreateQuery(Expression expression) => + new TypedFrame(this, expression); + + public IQueryable CreateQuery(Expression expression) => + throw new NotSupportedException("Non-generic CreateQuery is not used in tests."); + + public object Compile(Expression expression) => + throw new NotSupportedException( + "TestFrameProvider does not support compilation. Expression tree tests should " + + "inspect the Expression property directly." + ); + + public IEnumerable Materialize(Expression expression) => + throw new NotSupportedException( + "TestFrameProvider does not support materialization. Expression tree tests should " + + "inspect the Expression property directly." + ); + + public TResult Execute(Expression expression) => throw new NotSupportedException(); + + public object? Execute(Expression expression) => throw new NotSupportedException(); +} diff --git a/tests/Flowthru.Extensions.Spark.Tests/_Fixtures/TestSchemas.cs b/tests/Flowthru.Extensions.Spark.Tests/_Fixtures/TestSchemas.cs new file mode 100644 index 00000000..f70151f5 --- /dev/null +++ b/tests/Flowthru.Extensions.Spark.Tests/_Fixtures/TestSchemas.cs @@ -0,0 +1,145 @@ +using Flowthru.Core.Abstractions; + +namespace Flowthru.Extensions.Spark.Tests; + +/// +/// Simple flat schema for testing basic Where and Select operations. +/// +public record PersonSchema : IFlatSchema +{ + public required string Name { get; init; } + public required int Age { get; init; } + public required bool IsActive { get; init; } +} + +/// +/// Schema with to test column name resolution. +/// +public record LabeledSchema +{ + [SerializedLabel("full_name")] + public required string FullName { get; init; } + + [SerializedLabel("employee_id")] + public required int EmployeeId { get; init; } + + public required string Department { get; init; } +} + +/// +/// Projection target for Select tests. +/// +public record NameOnlySchema +{ + public required string Name { get; init; } +} + +/// +/// Projection target for Select tests with computed fields. +/// +public record PersonSummarySchema +{ + public required string Name { get; init; } + public required int Age { get; init; } +} + +/// +/// Schema for the "right" side of Join tests. +/// +public record DepartmentSchema +{ + public required string Name { get; init; } + public required int DeptId { get; init; } +} + +/// +/// Schema for the "left" side of Join tests — has a foreign key. +/// +public record EmployeeSchema +{ + public required string Name { get; init; } + public required int DeptId { get; init; } +} + +/// +/// Projection target for Join result. +/// +public record EmployeeDeptSchema +{ + public required string EmployeeName { get; init; } + public required string DepartmentName { get; init; } +} + +/// +/// Schema for GroupBy / Aggregate tests. +/// +public record SalesSchema +{ + public required string Category { get; init; } + public required double Amount { get; init; } + public required int Quantity { get; init; } +} + +/// +/// Aggregate result schema for GroupBy / Aggregate tests. +/// +public record SalesSummarySchema +{ + public required string Category { get; init; } + public required double TotalAmount { get; init; } + public required long TotalCount { get; init; } +} + +/// +/// Schema with a nullable reference property for null-check translation tests. +/// +public record OrderSchema +{ + public required string OrderId { get; init; } + public required string? Region { get; init; } + public required double Amount { get; init; } + public required DateTime OrderDate { get; init; } +} + +/// +/// Schema with numeric columns for Math method translation tests. +/// +public record MeasurementSchema +{ + public required double Value { get; init; } + public required double Temperature { get; init; } +} + +/// +/// Source schema for window function expression tests. +/// +public record StaffSchema +{ + public required string Name { get; init; } + public required string Department { get; init; } + public required double Salary { get; init; } + public required DateTime HireDate { get; init; } +} + +/// +/// Projection target for single-window SelectOver tests. +/// +public record StaffRankedSchema +{ + public required string Name { get; init; } + public required string Department { get; init; } + public required double Salary { get; init; } + public required long DeptRank { get; init; } + public required double RunningTotal { get; init; } + public required double? PrevSalary { get; init; } +} + +/// +/// Projection target for multi-window SelectOver tests. +/// +public record StaffMultiWindowSchema +{ + public required string Name { get; init; } + public required long DeptRank { get; init; } + public required long HireOrder { get; init; } +} diff --git a/tests/Flowthru.Tests.Examples/project.json b/tests/Flowthru.Tests.Examples/project.json index d7806f82..d1f97587 100644 --- a/tests/Flowthru.Tests.Examples/project.json +++ b/tests/Flowthru.Tests.Examples/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "application", "sourceRoot": "tests/Flowthru.Tests.Examples", - "implicitDependencies": [ "examples" ], + "implicitDependencies": [ ], "tags": [ "type:test", "scope:examples" ], "targets": { "test": { @@ -17,7 +17,7 @@ "forwardAllArgs": true }, "cache": true, - "inputs": [ "default", "^production" ] + "inputs": [ "default", "{workspaceRoot}/src/**", "{workspaceRoot}/examples/**" ] }, "test:discovery": { "//": "Run only test discovery tests (smoke check for example project wiring)", diff --git a/tests/Flowthru.Tests.Spark/AssemblyLoaderTests.cs b/tests/Flowthru.Tests.Spark/AssemblyLoaderTests.cs new file mode 100644 index 00000000..c0f6c138 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/AssemblyLoaderTests.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using Flowthru.Spark; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Utils; +using Moq; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + [Collection("Spark Unit Tests")] + public class AssemblyLoaderTests + { + private readonly Mock _mockJvm; + + public AssemblyLoaderTests(SparkFixture _fixture) + { + _mockJvm = _fixture.MockJvm; + } + + [Fact] + public void TestAssemblySearchPathResolver() + { + string sparkFilesDir = SparkFiles.GetRootDirectory(); + string curDir = Directory.GetCurrentDirectory(); + string appDir = AppDomain.CurrentDomain.BaseDirectory; + + // Test the default scenario. + string[] searchPaths = AssemblySearchPathResolver.GetAssemblySearchPaths(); + Assert.Equal(new[] { sparkFilesDir, curDir, appDir }, searchPaths); + + // Test the case where DOTNET_ASSEMBLY_SEARCH_PATHS is defined. + char sep = Path.PathSeparator; + Environment.SetEnvironmentVariable( + AssemblySearchPathResolver.AssemblySearchPathsEnvVarName, + $"mydir1, mydir2, .{sep}mydir3,.{sep}mydir4" + ); + + searchPaths = AssemblySearchPathResolver.GetAssemblySearchPaths(); + Assert.Equal( + new[] + { + "mydir1", + "mydir2", + Path.Combine(curDir, $".{sep}mydir3"), + Path.Combine(curDir, $".{sep}mydir4"), + sparkFilesDir, + curDir, + appDir, + }, + searchPaths + ); + + Environment.SetEnvironmentVariable( + AssemblySearchPathResolver.AssemblySearchPathsEnvVarName, + null + ); + } + + [Fact] + public void TestResolveAssemblyWithRelativePath() + { + _mockJvm + .Setup(m => m.CallStaticJavaMethod("org.apache.spark.SparkFiles", "getRootDirectory")) + .Returns("."); + + AssemblyLoader.LoadFromFile = AssemblyLoadContext.Default.LoadFromAssemblyPath; + Assembly expectedAssembly = Assembly.GetExecutingAssembly(); + Assembly actualAssembly = AssemblyLoader.ResolveAssembly(expectedAssembly.FullName); + + Assert.Equal(expectedAssembly, actualAssembly); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/BinarySerDeTests.cs b/tests/Flowthru.Tests.Spark/BinarySerDeTests.cs new file mode 100644 index 00000000..39544726 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/BinarySerDeTests.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using MessagePack; +using Flowthru.Spark.Utils; +using Xunit; + +namespace Flowthru.Tests.Spark; + +[Collection("Spark Unit Tests")] +public class BinarySerDeTests +{ + [Theory] + [InlineData(42)] + [InlineData("Test")] + [InlineData(99.99)] + public void Serialize_ShouldWriteObjectToStream(object input) + { + using var memoryStream = new MemoryStream(); + BinarySerDe.Serialize(memoryStream, input); + memoryStream.Position = 0; + + var deserializedObject = MessagePackSerializer.Typeless.Deserialize(memoryStream); + + Assert.Equal(input, deserializedObject); + } + + [Fact] + public void Deserialize_ShouldReturnExpectedObject_WhenTypeMatches() + { + var employee = new Employee { Id = 101, Name = "John Doe" }; + using var memoryStream = new MemoryStream(); + MessagePackSerializer.Typeless.Serialize(memoryStream, employee); + memoryStream.Position = 0; + + var result = BinarySerDe.Deserialize(memoryStream); + + Assert.Equal(employee.Id, result.Id); + Assert.Equal(employee.Name, result.Name); + } + + [Fact] + public void Deserialize_ShouldThrowInvalidCastEx_WhenTypeDoesNotMatch() + { + var employee = new Employee { Id = 101, Name = "John Doe" }; + using var memoryStream = new MemoryStream(); + MessagePackSerializer.Typeless.Serialize(memoryStream, employee); + memoryStream.Position = 0; + + var action = () => BinarySerDe.Deserialize(memoryStream); + + Assert.Throws(action); + } + + [Fact] + public void Serialize_CustomFunctionAndObject_ShouldBeSerializable() + { + var department = new Department { Name = "HR", EmployeeCount = 27 }; + var employeeStub = new Employee + { + EmbeddedObject = department, + Id = 11, + Name = "Derek", + }; + using var memoryStream = new MemoryStream(); + MessagePackSerializer.Typeless.Serialize(memoryStream, employeeStub); + memoryStream.Position = 0; + + var deserializedCalculation = BinarySerDe.Deserialize(memoryStream); + + Assert.IsType(deserializedCalculation.EmbeddedObject); + Assert.Equal(27, ((Department)deserializedCalculation.EmbeddedObject).EmployeeCount); + Assert.Equal("HR", ((Department)deserializedCalculation.EmbeddedObject).Name); + } + + [Fact] + public void Serialize_ClassWithoutSerializableAttribute_ShouldThrowException() + { + var nonSerializableClass = new NonSerializableClass { Value = 123 }; + using var memoryStream = new MemoryStream(); + BinarySerDe.Serialize(memoryStream, nonSerializableClass); + memoryStream.Position = 0; + + Assert.Throws(() => BinarySerDe.Deserialize(memoryStream)); + } + + [Fact] + public void Serialize_CollectionAndDictionary_ShouldBeSerializable() + { + var list = new List { 1, 2, 3 }; + var dictionary = new Dictionary { { "one", 1 }, { "two", 2 } }; + + using var memoryStream = new MemoryStream(); + BinarySerDe.Serialize(memoryStream, list); + memoryStream.Position = 0; + var deserializedList = MessagePackSerializer.Typeless.Deserialize(memoryStream) as List; + + Assert.Equal(list, deserializedList); + + memoryStream.SetLength(0); + BinarySerDe.Serialize(memoryStream, dictionary); + memoryStream.Position = 0; + var deserializedDictionary = MessagePackSerializer.Typeless.Deserialize(memoryStream) as Dictionary; + + Assert.Equal(dictionary, deserializedDictionary); + } + + [Fact] + public void Serialize_PolymorphicObject_ShouldBeSerializable() + { + Employee manager = new Manager { Id = 1, Name = "Alice", Role = "Account manager" }; + using var memoryStream = new MemoryStream(); + BinarySerDe.Serialize(memoryStream, manager); + memoryStream.Position = 0; + + var deserializedEmployee = BinarySerDe.Deserialize(memoryStream); + + Assert.IsType(deserializedEmployee); + Assert.Equal("Alice", deserializedEmployee.Name); + Assert.Equal("Account manager", ((Manager)deserializedEmployee).Role); + } + + [Serializable] + private class Employee + { + public int Id { get; set; } + + public string Name { get; set; } + + public object EmbeddedObject { get; set; } + } + + [Serializable] + private class Department + { + public string Name { get; set; } + public int EmployeeCount { get; set; } + } + + [Serializable] + private class Manager : Employee + { + public string Role { get; set; } + } + + private class NonSerializableClass + { + public int Value { get; init; } + } +} diff --git a/tests/Flowthru.Tests.Spark/CallbackTests.cs b/tests/Flowthru.Tests.Spark/CallbackTests.cs new file mode 100644 index 00000000..4445cc16 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/CallbackTests.cs @@ -0,0 +1,270 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Network; +using Moq; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + [Collection("Spark Unit Tests")] + public class CallbackTests + { + private readonly Mock _mockJvm; + + public CallbackTests(SparkFixture fixture) + { + _mockJvm = fixture.MockJvm; + } + + [Fact] + public async Task TestCallbackIds() + { + int numToRegister = 100; + var callbackServer = new CallbackServer(_mockJvm.Object, false); + var callbackHandler = new TestCallbackHandler(); + + var ids = new ConcurrentBag(); + var tasks = new List(); + for (int i = 0; i < numToRegister; ++i) + { + tasks.Add( + Task.Run(() => ids.Add(callbackServer.RegisterCallback(callbackHandler)))); + } + + await Task.WhenAll(tasks); + + IOrderedEnumerable actualIds = ids.OrderBy(i => i); + IEnumerable expectedIds = Enumerable.Range(1, numToRegister); + Assert.True(expectedIds.SequenceEqual(actualIds)); + } + + [Fact] + public void TestCallbackServer() + { + var callbackServer = new CallbackServer(_mockJvm.Object, false); + var callbackHandler = new TestCallbackHandler(); + + callbackHandler.Id = callbackServer.RegisterCallback(callbackHandler); + Assert.Equal(1, callbackHandler.Id); + + using ISocketWrapper callbackSocket = SocketFactory.CreateSocket(); + callbackServer.Run(callbackSocket); + + int connectionNumber = 2; + var clientSockets = new ISocketWrapper[connectionNumber]; + for (int i = 0; i < connectionNumber; ++i) + { + var ipEndpoint = (IPEndPoint)callbackSocket.LocalEndPoint; + ISocketWrapper clientSocket = SocketFactory.CreateSocket(); + clientSockets[i] = clientSocket; + clientSocket.Connect(ipEndpoint.Address, ipEndpoint.Port); + + WriteAndReadTestData(clientSocket, callbackHandler, i); + } + + Assert.Equal(connectionNumber, callbackServer.CurrentNumConnections); + + IOrderedEnumerable actualValues = callbackHandler.Inputs.OrderBy(i => i); + IEnumerable expectedValues = Enumerable + .Range(0, connectionNumber) + .Select(i => callbackHandler.Apply(i)) + .OrderBy(i => i); + Assert.True(expectedValues.SequenceEqual(actualValues)); + } + + [Fact] + public void TestCallbackHandlers() + { + var tokenSource = new CancellationTokenSource(); + var callbackHandlersDict = new ConcurrentDictionary(); + int inputToHandler = 1; + { + // Test CallbackConnection using a ICallbackHandler that runs + // normally without error. + var callbackHandler = new TestCallbackHandler + { + Id = 1 + }; + callbackHandlersDict[callbackHandler.Id] = callbackHandler; + TestCallbackConnection( + callbackHandlersDict, + callbackHandler, + inputToHandler, + tokenSource.Token); + Assert.Single(callbackHandler.Inputs); + Assert.Equal( + callbackHandler.Apply(inputToHandler), + callbackHandler.Inputs.First()); + } + { + // Test CallbackConnection using a ICallbackHandler that + // throws an exception. + var callbackHandler = new ThrowsExceptionHandler + { + Id = 2 + }; + callbackHandlersDict[callbackHandler.Id] = callbackHandler; + TestCallbackConnection( + callbackHandlersDict, + callbackHandler, + inputToHandler, + tokenSource.Token); + Assert.Empty(callbackHandler.Inputs); + } + { + // Test CallbackConnection when cancellation has been requested for the token. + tokenSource.Cancel(); + var callbackHandler = new TestCallbackHandler + { + Id = 3 + }; + callbackHandlersDict[callbackHandler.Id] = callbackHandler; + TestCallbackConnection( + callbackHandlersDict, + callbackHandler, + inputToHandler, + tokenSource.Token); + Assert.Empty(callbackHandler.Inputs); + } + } + + [Fact] + public void TestJvmCallbackClientProperty() + { + var server = new CallbackServer(_mockJvm.Object, run: false); + Assert.Throws(() => server.JvmCallbackClient); + + using ISocketWrapper callbackSocket = SocketFactory.CreateSocket(); + server.Run(callbackSocket); + Assert.NotNull(server.JvmCallbackClient); + } + + private void TestCallbackConnection( + ConcurrentDictionary callbackHandlersDict, + ITestCallbackHandler callbackHandler, + int inputToHandler, + CancellationToken token) + { + using ISocketWrapper serverListener = SocketFactory.CreateSocket(); + serverListener.Listen(); + + var ipEndpoint = (IPEndPoint)serverListener.LocalEndPoint; + using ISocketWrapper clientSocket = SocketFactory.CreateSocket(); + clientSocket.Connect(ipEndpoint.Address, ipEndpoint.Port); + + // Don't use "using" here. The CallbackConnection will dispose the socket. + ISocketWrapper serverSocket = serverListener.Accept(); + var callbackConnection = new CallbackConnection(0, serverSocket, callbackHandlersDict); + Task task = Task.Run(() => callbackConnection.Run(token)); + + if (token.IsCancellationRequested) + { + task.Wait(); + Assert.False(callbackConnection.IsRunning); + } + else + { + WriteAndReadTestData(clientSocket, callbackHandler, inputToHandler); + + if (callbackHandler.Throws) + { + task.Wait(); + Assert.False(callbackConnection.IsRunning); + } + else + { + Assert.True(callbackConnection.IsRunning); + + // Clean up CallbackConnection + Stream outputStream = clientSocket.OutputStream; + SerDe.Write(outputStream, (int)CallbackConnection.ConnectionStatus.REQUEST_CLOSE); + outputStream.Flush(); + task.Wait(); + Assert.False(callbackConnection.IsRunning); + } + } + } + + private void WriteAndReadTestData( + ISocketWrapper socket, + ITestCallbackHandler callbackHandler, + int inputToHandler) + { + Stream inputStream = socket.InputStream; + Stream outputStream = socket.OutputStream; + + SerDe.Write(outputStream, (int)CallbackFlags.CALLBACK); + SerDe.Write(outputStream, callbackHandler.Id); + SerDe.Write(outputStream, sizeof(int)); + SerDe.Write(outputStream, inputToHandler); + SerDe.Write(outputStream, (int)CallbackFlags.END_OF_STREAM); + outputStream.Flush(); + + int callbackFlag = SerDe.ReadInt32(inputStream); + if (callbackFlag == (int)CallbackFlags.DOTNET_EXCEPTION_THROWN) + { + string exceptionMessage = SerDe.ReadString(inputStream); + Assert.False(string.IsNullOrEmpty(exceptionMessage)); + Assert.Contains(callbackHandler.ExceptionMessage, exceptionMessage); + } + else + { + Assert.Equal((int)CallbackFlags.END_OF_STREAM, callbackFlag); + } + } + + private class TestCallbackHandler : ICallbackHandler, ITestCallbackHandler + { + public void Run(Stream inputStream) => Inputs.Add(Apply(SerDe.ReadInt32(inputStream))); + + public ConcurrentBag Inputs { get; } = new ConcurrentBag(); + + public int Id { get; set; } + + public bool Throws { get; } = false; + + public string ExceptionMessage => throw new NotImplementedException(); + + public int Apply(int i) => 10 * i; + } + + private class ThrowsExceptionHandler : ICallbackHandler, ITestCallbackHandler + { + public void Run(Stream inputStream) => throw new Exception(ExceptionMessage); + + public ConcurrentBag Inputs { get; } = new ConcurrentBag(); + + public int Id { get; set; } + + public bool Throws { get; } = true; + + public string ExceptionMessage { get; } = "Dotnet Callback Handler Exception Message"; + + public int Apply(int i) => throw new NotImplementedException(); + } + + private interface ITestCallbackHandler + { + ConcurrentBag Inputs { get; } + + int Id { get; set; } + + bool Throws { get; } + + string ExceptionMessage { get; } + + int Apply(int i); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/CollectionUtilsTests.cs b/tests/Flowthru.Tests.Spark/CollectionUtilsTests.cs new file mode 100644 index 00000000..cb07592a --- /dev/null +++ b/tests/Flowthru.Tests.Spark/CollectionUtilsTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Flowthru.Spark.Utils; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + public class CollectionUtilsTests + { + [Fact] + public void TestArrayEquals() + { + Assert.False(CollectionUtils.ArrayEquals(new int[] { 1 }, null)); + Assert.False(CollectionUtils.ArrayEquals(null, new int[] { 1 })); + Assert.False(CollectionUtils.ArrayEquals(new int[] { }, new int[] { 1 })); + Assert.False(CollectionUtils.ArrayEquals(new int[] { 1 }, new int[] { })); + Assert.False(CollectionUtils.ArrayEquals(new int[] { 1 }, new int[] { 1, 2 })); + Assert.False(CollectionUtils.ArrayEquals(new int[] { 1 }, new int[] { 2 })); + + Assert.True(CollectionUtils.ArrayEquals(null, null)); + Assert.True(CollectionUtils.ArrayEquals(new int[] { 1 }, new int[] { 1 })); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/CommandSerDeTests.cs b/tests/Flowthru.Tests.Spark/CommandSerDeTests.cs new file mode 100644 index 00000000..bc44c032 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/CommandSerDeTests.cs @@ -0,0 +1,170 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Apache.Arrow; +using Flowthru.Spark; +using Flowthru.Spark.Sql; +using Microsoft.Data.Analysis; +using Xunit; +using static Flowthru.Spark.Utils.CommandSerDe; +using static Flowthru.Tests.Spark.TestUtils.ArrowTestUtils; +using RDDWorkerFunction = Flowthru.Spark.RDD.WorkerFunction; + +namespace Flowthru.Tests.Spark +{ + [Collection("Spark Unit Tests")] + public class CommandSerDeTests + { + [Fact] + public void TestCommandSerDeForSqlPickling() + { + var udfWrapper = new PicklingUdfWrapper((str) => $"hello {str}"); + var workerFunction = new PicklingWorkerFunction(udfWrapper.Execute); + + byte[] serializedCommand = Serialize( + workerFunction.Func, + SerializedMode.Row, + SerializedMode.Row + ); + + using var ms = new MemoryStream(serializedCommand); + var deserializedWorkerFunction = new PicklingWorkerFunction( + Deserialize( + ms, + out SerializedMode serializerMode, + out SerializedMode deserializerMode, + out var runMode + ) + ); + + Assert.Equal(SerializedMode.Row, serializerMode); + Assert.Equal(SerializedMode.Row, deserializerMode); + Assert.Equal("N", runMode); + + object result = deserializedWorkerFunction.Func(0, new[] { "spark" }, new[] { 0 }); + Assert.Equal("hello spark", result); + } + + [Fact] + public void TestCommandSerDeForSqlArrow() + { + var udfWrapper = new ArrowUdfWrapper( + (strings) => + (StringArray)ToArrowArray( + Enumerable + .Range(0, strings.Length) + .Select(i => $"hello {strings.GetString(i)}") + .ToArray() + ) + ); + + var workerFunction = new ArrowWorkerFunction(udfWrapper.Execute); + + byte[] serializedCommand = Serialize( + workerFunction.Func, + SerializedMode.Row, + SerializedMode.Row + ); + + using var ms = new MemoryStream(serializedCommand); + var deserializedWorkerFunction = new ArrowWorkerFunction( + Deserialize( + ms, + out SerializedMode serializerMode, + out SerializedMode deserializerMode, + out var runMode + ) + ); + + Assert.Equal(SerializedMode.Row, serializerMode); + Assert.Equal(SerializedMode.Row, deserializerMode); + Assert.Equal("N", runMode); + + IArrowArray input = ToArrowArray(new[] { "spark" }); + IArrowArray result = deserializedWorkerFunction.Func(new[] { input }, new[] { 0 }); + AssertEquals("hello spark", result); + } + + [Fact] + public void TestCommandSerDeForSqlArrowDataFrame() + { + var udfWrapper = new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >((strings) => strings.Apply(cur => $"hello {cur}")); + + var workerFunction = new DataFrameWorkerFunction(udfWrapper.Execute); + + byte[] serializedCommand = Serialize( + workerFunction.Func, + SerializedMode.Row, + SerializedMode.Row + ); + + using var ms = new MemoryStream(serializedCommand); + var deserializedWorkerFunction = new DataFrameWorkerFunction( + Deserialize( + ms, + out SerializedMode serializerMode, + out SerializedMode deserializerMode, + out var runMode + ) + ); + + Assert.Equal(SerializedMode.Row, serializerMode); + Assert.Equal(SerializedMode.Row, deserializerMode); + Assert.Equal("N", runMode); + + var column = (StringArray)ToArrowArray(new[] { "spark" }); + + ArrowStringDataFrameColumn ArrowStringDataFrameColumn = ToArrowStringDataFrameColumn(column); + DataFrameColumn result = deserializedWorkerFunction.Func( + new[] { ArrowStringDataFrameColumn }, + new[] { 0 } + ); + AssertEquals("hello spark", result); + } + + [Fact] + public void TestCommandSerDeForRDD() + { + // Construct the UDF tree such that func1, func2, and func3 + // are executed in that order. + var func1 = new RDDWorkerFunction(new RDD.MapUdfWrapper((a) => a + 3).Execute); + + var func2 = new RDDWorkerFunction(new RDD.MapUdfWrapper((a) => a * 2).Execute); + + var func3 = new RDDWorkerFunction(new RDD.MapUdfWrapper((a) => a + 5).Execute); + + RDDWorkerFunction chainedFunc1 = RDDWorkerFunction.Chain(func1, func2); + RDDWorkerFunction chainedFunc2 = RDDWorkerFunction.Chain(chainedFunc1, func3); + + byte[] serializedCommand = Serialize( + chainedFunc2.Func, + SerializedMode.Byte, + SerializedMode.Byte + ); + + using var ms = new MemoryStream(serializedCommand); + var deserializedWorkerFunction = new RDDWorkerFunction( + Deserialize( + ms, + out SerializedMode serializerMode, + out SerializedMode deserializerMode, + out var runMode + ) + ); + + Assert.Equal(SerializedMode.Byte, serializerMode); + Assert.Equal(SerializedMode.Byte, deserializerMode); + Assert.Equal("N", runMode); + + IEnumerable result = deserializedWorkerFunction.Func(0, new object[] { 1, 2, 3 }); + Assert.Equal(new[] { 13, 15, 17 }, result.Cast()); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/ConfigurationServiceTests.cs b/tests/Flowthru.Tests.Spark/ConfigurationServiceTests.cs new file mode 100644 index 00000000..9fd4ec10 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/ConfigurationServiceTests.cs @@ -0,0 +1,175 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Flowthru.Spark.Services; +using Flowthru.Spark.Utils; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + public class ConfigurationServiceTests : IDisposable + { + private readonly WorkerDirEnvVars _workerDirEnvVars; + + public ConfigurationServiceTests() + { + var version = new Version(AssemblyInfoProvider.MicrosoftSparkAssemblyInfo().AssemblyVersion); + _workerDirEnvVars = new WorkerDirEnvVars + { + WorkerDir = new EnvVar(ConfigurationService.DefaultWorkerDirEnvVarName), + WorkerMajorMinorBuildDir = new EnvVar( + string.Format( + ConfigurationService.WorkerVerDirEnvVarNameFormat, + $"{version.Major}_{version.Minor}_{version.Build}")), + WorkerMajorMinorDir = new EnvVar( + string.Format( + ConfigurationService.WorkerVerDirEnvVarNameFormat, + $"{version.Major}_{version.Minor}")), + WorkerMajorDir = new EnvVar( + string.Format(ConfigurationService.WorkerVerDirEnvVarNameFormat, version.Major)) + }; + + Environment.SetEnvironmentVariable(_workerDirEnvVars.WorkerDir.Name, null); + Environment.SetEnvironmentVariable(_workerDirEnvVars.WorkerMajorMinorBuildDir.Name, null); + Environment.SetEnvironmentVariable(_workerDirEnvVars.WorkerMajorMinorDir.Name, null); + Environment.SetEnvironmentVariable(_workerDirEnvVars.WorkerMajorDir.Name, null); + } + + [Fact] + public void TestWorkerExePathWithNoEnvVars() + { + var configService = new ConfigurationService(); + + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorBuildDir.Name)); + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorDir.Name)); + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorDir.Name)); + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerDir.Name)); + + // Environment variables not set, only Microsoft.Spark.Worker filename should be returned. + Assert.Equal(ConfigurationService.ProcFileName, configService.GetWorkerExePath()); + } + + [Fact] + public void TestWorkerExePathWithWorkerDirEnvVar() + { + var configService = new ConfigurationService(); + string workerDir = "workerDir"; + Environment.SetEnvironmentVariable(_workerDirEnvVars.WorkerDir.Name, workerDir); + + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorBuildDir.Name)); + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorDir.Name)); + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorDir.Name)); + Assert.True(IsEnvVarSet(_workerDirEnvVars.WorkerDir.Name)); + + // Only WorkerDir is set, WorkerExePath will be built using it. + Assert.Equal( + Path.Combine(workerDir, ConfigurationService.ProcFileName), + configService.GetWorkerExePath()); + } + + [Fact] + public void TestWorkerExePathWithEnvVarPrecedence() + { + { + var configService = new ConfigurationService(); + string workerDir = "workerDir"; + Environment.SetEnvironmentVariable(_workerDirEnvVars.WorkerDir.Name, workerDir); + + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorBuildDir.Name)); + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorDir.Name)); + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorDir.Name)); + Assert.True(IsEnvVarSet(_workerDirEnvVars.WorkerDir.Name)); + Assert.Equal( + Path.Combine(workerDir, ConfigurationService.ProcFileName), + configService.GetWorkerExePath()); + } + + { + var configService = new ConfigurationService(); + string workerMajorDir = "workerMajorDir"; + Environment.SetEnvironmentVariable(_workerDirEnvVars.WorkerMajorDir.Name, workerMajorDir); + + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorBuildDir.Name)); + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorDir.Name)); + Assert.True(IsEnvVarSet(_workerDirEnvVars.WorkerMajorDir.Name)); + Assert.True(IsEnvVarSet(_workerDirEnvVars.WorkerDir.Name)); + Assert.Equal( + Path.Combine(workerMajorDir, ConfigurationService.ProcFileName), + configService.GetWorkerExePath()); + } + + { + var configService = new ConfigurationService(); + string workerMajorMinorDir = "workerMajorMinorDir"; + Environment.SetEnvironmentVariable( + _workerDirEnvVars.WorkerMajorMinorDir.Name, workerMajorMinorDir); + + Assert.False(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorBuildDir.Name)); + Assert.True(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorDir.Name)); + Assert.True(IsEnvVarSet(_workerDirEnvVars.WorkerMajorDir.Name)); + Assert.True(IsEnvVarSet(_workerDirEnvVars.WorkerDir.Name)); + Assert.Equal( + Path.Combine(workerMajorMinorDir, ConfigurationService.ProcFileName), + configService.GetWorkerExePath()); + } + + { + var configService = new ConfigurationService(); + string workerMajorMinorBuildDir = "workerMajorMinorBuildDir"; + Environment.SetEnvironmentVariable( + _workerDirEnvVars.WorkerMajorMinorBuildDir.Name, workerMajorMinorBuildDir); + + Assert.True(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorBuildDir.Name)); + Assert.True(IsEnvVarSet(_workerDirEnvVars.WorkerMajorMinorDir.Name)); + Assert.True(IsEnvVarSet(_workerDirEnvVars.WorkerMajorDir.Name)); + Assert.True(IsEnvVarSet(_workerDirEnvVars.WorkerDir.Name)); + Assert.Equal( + Path.Combine(workerMajorMinorBuildDir, ConfigurationService.ProcFileName), + configService.GetWorkerExePath()); + } + } + + public void Dispose() + { + Environment.SetEnvironmentVariable( + _workerDirEnvVars.WorkerDir.Name, + _workerDirEnvVars.WorkerDir.Value); + Environment.SetEnvironmentVariable( + _workerDirEnvVars.WorkerMajorMinorBuildDir.Name, + _workerDirEnvVars.WorkerMajorMinorBuildDir.Value); + Environment.SetEnvironmentVariable( + _workerDirEnvVars.WorkerMajorMinorDir.Name, + _workerDirEnvVars.WorkerMajorMinorDir.Value); + Environment.SetEnvironmentVariable( + _workerDirEnvVars.WorkerMajorDir.Name, + _workerDirEnvVars.WorkerMajorDir.Value); + } + + public bool IsEnvVarSet(string name) => + !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(name)); + + private class WorkerDirEnvVars + { + public EnvVar WorkerDir { get; set; } + public EnvVar WorkerMajorMinorBuildDir { get; set; } + public EnvVar WorkerMajorMinorDir { get; set; } + public EnvVar WorkerMajorDir { get; set; } + } + + private class EnvVar + { + public string Name { get; } + public string Value { get; } + + public EnvVar(string name) + { + Name = name; + Value = Environment.GetEnvironmentVariable(name); + } + + } + } +} diff --git a/tests/Flowthru.Tests.Spark/DependencyProviderUtilsTests.cs b/tests/Flowthru.Tests.Spark/DependencyProviderUtilsTests.cs new file mode 100644 index 00000000..5aa68fd7 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/DependencyProviderUtilsTests.cs @@ -0,0 +1,156 @@ +using System; +using System.IO; +using System.Linq; +using Flowthru.Tests.Spark.TestUtils; +using Flowthru.Spark.Utils; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + public class DependencyProviderUtilsTests + { + [Fact] + public void TestNuGetMetadataEquals() + { + string expectedFileName = "package.name.1.0.0.nupkg"; + string expectedPackageName = "package.name"; + string expectedPackageVersion = "1.0.0"; + + var nugetMetadata = new DependencyProviderUtils.NuGetMetadata + { + FileName = expectedFileName, + PackageName = expectedPackageName, + PackageVersion = expectedPackageVersion + }; + + Assert.False(nugetMetadata.Equals(null)); + Assert.False(nugetMetadata.Equals(new DependencyProviderUtils.NuGetMetadata())); + Assert.False(nugetMetadata.Equals(new DependencyProviderUtils.NuGetMetadata + { + FileName = "", + PackageName = expectedPackageName, + PackageVersion = expectedPackageVersion + })); + Assert.False(nugetMetadata.Equals(new DependencyProviderUtils.NuGetMetadata + { + FileName = expectedFileName, + PackageName = "", + PackageVersion = expectedPackageVersion + })); + Assert.False(nugetMetadata.Equals(new DependencyProviderUtils.NuGetMetadata + { + FileName = expectedFileName, + PackageName = expectedPackageName, + PackageVersion = "" + })); + + Assert.True(nugetMetadata.Equals(new DependencyProviderUtils.NuGetMetadata + { + FileName = expectedFileName, + PackageName = expectedPackageName, + PackageVersion = expectedPackageVersion + })); + } + + [Fact] + public void TestMetadataEquals() + { + string expectedAssemblyProbingPath = "/assembly/probe/path"; + string expectedNativeProbingPath = "/native/probe/path"; + var expectedNugetMetadata = new DependencyProviderUtils.NuGetMetadata + { + FileName = "package.name.1.0.0.nupkg", + PackageName = "package.name", + PackageVersion = "1.0.0" + }; + + var metadata = new DependencyProviderUtils.Metadata + { + AssemblyProbingPaths = new string[] { expectedAssemblyProbingPath }, + NativeProbingPaths = new string[] { expectedNativeProbingPath }, + NuGets = new DependencyProviderUtils.NuGetMetadata[] { expectedNugetMetadata } + }; + + Assert.False(metadata.Equals(null)); + Assert.False(metadata.Equals(new DependencyProviderUtils.Metadata())); + Assert.False(metadata.Equals(new DependencyProviderUtils.Metadata + { + AssemblyProbingPaths = new string[] { expectedAssemblyProbingPath }, + NativeProbingPaths = new string[] { expectedNativeProbingPath, "" }, + NuGets = new DependencyProviderUtils.NuGetMetadata[] { expectedNugetMetadata } + })); + Assert.False(metadata.Equals(new DependencyProviderUtils.Metadata + { + AssemblyProbingPaths = new string[] { expectedAssemblyProbingPath }, + NativeProbingPaths = new string[] { expectedNativeProbingPath }, + NuGets = new DependencyProviderUtils.NuGetMetadata[] { expectedNugetMetadata, null } + })); + Assert.False(metadata.Equals(new DependencyProviderUtils.Metadata + { + AssemblyProbingPaths = new string[] { expectedAssemblyProbingPath, "" }, + NativeProbingPaths = new string[] { expectedNativeProbingPath }, + NuGets = new DependencyProviderUtils.NuGetMetadata[] { expectedNugetMetadata } + })); + + Assert.True(metadata.Equals(new DependencyProviderUtils.Metadata + { + AssemblyProbingPaths = new string[] { expectedAssemblyProbingPath }, + NativeProbingPaths = new string[] { expectedNativeProbingPath }, + NuGets = new DependencyProviderUtils.NuGetMetadata[] { expectedNugetMetadata } + })); + } + + [Fact] + public void TestMetadataSerDe() + { + using var tempDir = new TemporaryDirectory(); + var metadata = new DependencyProviderUtils.Metadata + { + AssemblyProbingPaths = new string[] { "/assembly/probe/path" }, + NativeProbingPaths = new string[] { "/native/probe/path" }, + NuGets = new DependencyProviderUtils.NuGetMetadata[] + { + new DependencyProviderUtils.NuGetMetadata + { + FileName = "package.name.1.0.0.nupkg", + PackageName = "package.name", + PackageVersion = "1.0.0" + } + } + }; + + string serializedFilePath = Path.Combine(tempDir.Path, "serializedMetadata"); + metadata.Serialize(serializedFilePath); + + DependencyProviderUtils.Metadata deserializedMetadata = + DependencyProviderUtils.Metadata.Deserialize(serializedFilePath); + + Assert.True(metadata.Equals(deserializedMetadata)); + } + + [Fact] + public void TestFileNames() + { + Guid runId = Guid.NewGuid(); + using var tempDir = new TemporaryDirectory(); + foreach (long num in Enumerable.Range(0, 3).Select(x => System.Math.Pow(10, x))) + { + string filePath = + Path.Combine(tempDir.Path, DependencyProviderUtils.CreateFileName(runId, num)); + File.Create(filePath).Dispose(); + } + + var expectedFiles = new string[] + { + $"dependencyProviderMetadata_{runId.ToString("N").Substring(0, 8)}00000000001", + $"dependencyProviderMetadata_{runId.ToString("N").Substring(0, 8)}00000000010", + $"dependencyProviderMetadata_{runId.ToString("N").Substring(0, 8)}00000000100", + }; + IOrderedEnumerable actualFiles = DependencyProviderUtils + .GetMetadataFiles(tempDir.Path) + .Select(f => Path.GetFileName(f)) + .OrderBy(s => s); + Assert.True(expectedFiles.SequenceEqual(actualFiles)); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/Flowthru.Tests.Spark.csproj b/tests/Flowthru.Tests.Spark/Flowthru.Tests.Spark.csproj new file mode 100644 index 00000000..0b41955a --- /dev/null +++ b/tests/Flowthru.Tests.Spark/Flowthru.Tests.Spark.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + Flowthru.Tests.Spark + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/Flowthru.Tests.Spark/LICENSE-DOTNET-SPARK b/tests/Flowthru.Tests.Spark/LICENSE-DOTNET-SPARK new file mode 100644 index 00000000..ec713658 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/LICENSE-DOTNET-SPARK @@ -0,0 +1,23 @@ +The MIT License (MIT) + +[Copyright (c) .NET Foundation and Contributors](https://github.com/dotnet/spark/blob/main/LICENSE) + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/Flowthru.Tests.Spark/SerDeTests.cs b/tests/Flowthru.Tests.Spark/SerDeTests.cs new file mode 100644 index 00000000..52b906cc --- /dev/null +++ b/tests/Flowthru.Tests.Spark/SerDeTests.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Flowthru.Spark.Interop.Ipc; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + public class SerDeTests + { + [Fact] + public void TestReadAndWrite() + { + using (var ms = new MemoryStream()) + { + // Test bool. + SerDe.Write(ms, true); + ms.Seek(0, SeekOrigin.Begin); + Assert.True(SerDe.ReadBool(ms)); + ms.Seek(0, SeekOrigin.Begin); + + SerDe.Write(ms, false); + ms.Seek(0, SeekOrigin.Begin); + Assert.False(SerDe.ReadBool(ms)); + ms.Seek(0, SeekOrigin.Begin); + + // Test int. + SerDe.Write(ms, 12345); + ms.Seek(0, SeekOrigin.Begin); + Assert.Equal(12345, SerDe.ReadInt32(ms)); + ms.Seek(0, SeekOrigin.Begin); + + // Test long. + SerDe.Write(ms, 123456789000); + ms.Seek(0, SeekOrigin.Begin); + Assert.Equal(123456789000, SerDe.ReadInt64(ms)); + ms.Seek(0, SeekOrigin.Begin); + + // Test double. + SerDe.Write(ms, Math.PI); + ms.Seek(0, SeekOrigin.Begin); + Assert.Equal(Math.PI, SerDe.ReadDouble(ms)); + ms.Seek(0, SeekOrigin.Begin); + + // Test string. + SerDe.Write(ms, "hello world!"); + ms.Seek(0, SeekOrigin.Begin); + Assert.Equal("hello world!", SerDe.ReadString(ms)); + ms.Seek(0, SeekOrigin.Begin); + } + } + + [Fact] + public void TestReadBytes() + { + // Test the case where invalid length is given. + Assert.Throws( + () => SerDe.ReadBytes(new MemoryStream(), -1)); + + // Test reading null length. + var ms = new MemoryStream(); + SerDe.Write(ms, (int)SpecialLengths.NULL); + ms.Seek(0, SeekOrigin.Begin); + Assert.Null(SerDe.ReadBytes(ms)); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/SparkFixture.cs b/tests/Flowthru.Tests.Spark/SparkFixture.cs new file mode 100644 index 00000000..32e66535 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/SparkFixture.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Flowthru.Spark.Interop; +using Flowthru.Spark.Interop.Ipc; +using Moq; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + public sealed class SparkFixture : IDisposable + { + internal Mock MockJvm { get; private set; } + + public SparkFixture() + { + SetupBasicMockJvm(); + + // Unit tests may contain calls that hit the AssemblyLoader. + // One of the AssemblyLoader assembly search paths is populated + // using SparkFiles. Unless we are running in an E2E scenario and + // on the Worker, SparkFiles will attempt to call the JVM. Because + // this is a (non E2E) Unit test, it is necessary to mock this call. + SetupSparkFiles(); + + var mockJvmBridgeFactory = new Mock(); + mockJvmBridgeFactory + .Setup(m => m.Create(It.IsAny())) + .Returns(MockJvm.Object); + + SparkEnvironment.JvmBridgeFactory = mockJvmBridgeFactory.Object; + } + + public void Dispose() + { + } + + private void SetupBasicMockJvm() + { + MockJvm = new Mock(); + + MockJvm + .Setup(m => m.CallStaticJavaMethod( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns( + new JvmObjectReference("result", MockJvm.Object)); + MockJvm + .Setup(m => m.CallStaticJavaMethod( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns( + new JvmObjectReference("result", MockJvm.Object)); + MockJvm + .Setup(m => m.CallStaticJavaMethod( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns( + new JvmObjectReference("result", MockJvm.Object)); + + MockJvm + .Setup(m => m.CallNonStaticJavaMethod( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns( + new JvmObjectReference("result", MockJvm.Object)); + MockJvm + .Setup(m => m.CallNonStaticJavaMethod( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns( + new JvmObjectReference("result", MockJvm.Object)); + MockJvm + .Setup(m => m.CallNonStaticJavaMethod( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns( + new JvmObjectReference("result", MockJvm.Object)); + } + + private void SetupSparkFiles() + { + MockJvm + .Setup(m => m.CallStaticJavaMethod( + "org.apache.spark.SparkFiles", + "getRootDirectory")) + .Returns("SparkFilesRootDirectory"); + } + } + + [CollectionDefinition("Spark Unit Tests")] + public class SparkCollection : ICollectionFixture + { + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + } +} diff --git a/tests/Flowthru.Tests.Spark/Sql/ColumnTests.cs b/tests/Flowthru.Tests.Spark/Sql/ColumnTests.cs new file mode 100644 index 00000000..be018e0f --- /dev/null +++ b/tests/Flowthru.Tests.Spark/Sql/ColumnTests.cs @@ -0,0 +1,492 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Expressions; +using Moq; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + [Collection("Spark Unit Tests")] + public class ColumnTests + { + private readonly Mock _mockJvm; + + public ColumnTests(SparkFixture fixture) + { + _mockJvm = fixture.MockJvm; + } + + private static JvmObjectId GetId(IJvmObjectReferenceProvider provider) => provider.Reference.Id; + + [Fact] + public void TestColumnNegateOperator() + { + Column column1 = CreateColumn("col1"); + Column column2 = -column1; + + _mockJvm.Verify(m => m.CallStaticJavaMethod( + "org.apache.spark.sql.functions", + "negate", + column1), Times.Once); + + Assert.Equal("result", GetId(column2)); + } + + [Fact] + public void TestNotOperator() + { + Column column1 = CreateColumn("col1"); + Column column2 = !column1; + + _mockJvm.Verify(m => m.CallStaticJavaMethod( + "org.apache.spark.sql.functions", + "not", + column1), Times.Once); + + Assert.Equal("result", GetId(column2)); + } + + [Fact] + public void TestEqualOperator() + { + { + // Column as a right operand. + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 == column2; + VerifyNonStaticCall(column1, "equalTo", column2); + Assert.Equal("result", GetId(result)); + } + { + // String as a right operand. + // Note that any object can be used in place of string. + Column column1 = CreateColumn("col1"); + Column result = column1 == "abc"; + VerifyNonStaticCall(column1, "equalTo", "abc"); + Assert.Equal("result", GetId(result)); + } + } + + [Fact] + public void TestNotEqualOperator() + { + { + // Column as a right operand. + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 != column2; + VerifyNonStaticCall(column1, "notEqual", column2); + Assert.Equal("result", GetId(result)); + } + { + // String as a right operand. + // Note that any object can be used in place of string. + Column column1 = CreateColumn("col1"); + Column result = column1 != "abc"; + VerifyNonStaticCall(column1, "notEqual", "abc"); + Assert.Equal("result", GetId(result)); + } + } + + [Fact] + public void TestGreaterThanOperator() + { + { + // Column as a right operand. + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 > column2; + VerifyNonStaticCall(column1, "gt", column2); + Assert.Equal("result", GetId(result)); + } + { + // String as a right operand. + // Note that any object can be used in place of string. + Column column1 = CreateColumn("col1"); + Column result = column1 > "abc"; + VerifyNonStaticCall(column1, "gt", "abc"); + Assert.Equal("result", GetId(result)); + } + } + + [Fact] + public void TestLessThanOperator() + { + { + // Column as a right operand. + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 < column2; + VerifyNonStaticCall(column1, "lt", column2); + Assert.Equal("result", GetId(result)); + } + { + // String as a right operand. + // Note that any object can be used in place of string. + Column column1 = CreateColumn("col1"); + Column result = column1 < "abc"; + VerifyNonStaticCall(column1, "lt", "abc"); + Assert.Equal("result", GetId(result)); + } + } + + [Fact] + public void TestLessThanEqualToOperator() + { + { + // Column as a right operand. + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 <= column2; + VerifyNonStaticCall(column1, "leq", column2); + Assert.Equal("result", GetId(result)); + } + { + // String as a right operand. + // Note that any object can be used in place of string. + Column column1 = CreateColumn("col1"); + Column result = column1 <= "abc"; + VerifyNonStaticCall(column1, "leq", "abc"); + Assert.Equal("result", GetId(result)); + } + } + + [Fact] + public void TestGreaterThanEqualToOperator() + { + { + // Column as a right operand. + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 >= column2; + VerifyNonStaticCall(column1, "geq", column2); + Assert.Equal("result", GetId(result)); + } + { + // String as a right operand. + // Note that any object can be used in place of string. + Column column1 = CreateColumn("col1"); + Column result = column1 >= "abc"; + VerifyNonStaticCall(column1, "geq", "abc"); + Assert.Equal("result", GetId(result)); + } + } + + [Fact] + public void TestAndOperator() + { + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 & column2; + VerifyNonStaticCall(column1, "and", column2); + Assert.Equal("result", GetId(result)); + } + + [Fact] + public void TestOrOperator() + { + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 | column2; + VerifyNonStaticCall(column1, "or", column2); + Assert.Equal("result", GetId(result)); + } + + [Fact] + public void TestPlusOperator() + { + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 + column2; + VerifyNonStaticCall(column1, "plus", column2); + Assert.Equal("result", GetId(result)); + } + + [Fact] + public void TestMinusOperator() + { + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 - column2; + VerifyNonStaticCall(column1, "minus", column2); + Assert.Equal("result", GetId(result)); + } + + [Fact] + public void TestMultiplyOperator() + { + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 * column2; + VerifyNonStaticCall(column1, "multiply", column2); + Assert.Equal("result", GetId(result)); + } + + [Fact] + public void TestDivideOperator() + { + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 / column2; + VerifyNonStaticCall(column1, "divide", column2); + Assert.Equal("result", GetId(result)); + } + + [Fact] + public void TestModOperator() + { + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + Column result = column1 % column2; + VerifyNonStaticCall(column1, "mod", column2); + } + + [Fact] + public void TestWhenCondition() + { + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + int value = 0; + column1.When(column2, value); + VerifyNonStaticCall(column1, "when", column2, value); + } + + [Fact] + public void TestBetweenCondition() + { + Column column1 = CreateColumn("col1"); + int val1 = 1; + int val2 = 2; + column1.Between(val1, val2); + VerifyNonStaticCall(column1, "between", val1, val2); + } + + [Fact] + public void TestSubStr() + { + { + Column column1 = CreateColumn("col1"); + int pos = 1; + int len = 2; + column1.SubStr(pos, len); + VerifyNonStaticCall(column1, "substr", pos, len); + } + { + Column column1 = CreateColumn("col1"); + Column pos = CreateColumn("col2"); + Column len = CreateColumn("col3"); + column1.SubStr(pos, len); + VerifyNonStaticCall(column1, "substr", pos, len); + } + } + + [Fact] + public void TestOver() + { + { + Column column1 = CreateColumn("col1"); + var windowSpec = + new WindowSpec(new JvmObjectReference("windowSpec", _mockJvm.Object)); + column1.Over(); + VerifyNonStaticCall(column1, "over"); + } + { + Column column1 = CreateColumn("col1"); + var windowSpec = + new WindowSpec(new JvmObjectReference("windowSpec", _mockJvm.Object)); + column1.Over(windowSpec); + VerifyNonStaticCall(column1, "over", windowSpec); + } + } + + [Fact] + public void TestIsIn() + { + { + var expected = new List {"vararg_1", "vararg_2"}; + Column column1 = CreateColumn("col1"); + column1.IsIn("vararg_1", "vararg_2"); + + VerifyNonStaticCall(column1, "isin", expected); + } + { + Column column1 = CreateColumn("col1"); + var expected = new List(){0, 1, 99}; + column1.IsIn(0, 1, 99); + VerifyNonStaticCall(column1, "isin", expected); + } + { + Column column1 = CreateColumn("col1"); + var expected = new List(){0L, 1L, 99L}; + column1.IsIn(0L, 1L, 99L); + VerifyNonStaticCall(column1, "isin", expected); + } + { + Column column1 = CreateColumn("col1"); + var expected = new List(){true, false}; + column1.IsIn(true, false); + VerifyNonStaticCall(column1, "isin", expected); + } + { + Column column1 = CreateColumn("col1"); + short short1 = 1; + short short2 = 2; + short short3 = 99; + + var expected = new List(){short1, short2, short3}; + column1.IsIn(short1, short2, short3); + VerifyNonStaticCall(column1, "isin", expected); + } + { + Column column1 = CreateColumn("col1"); + var expected = new List(){0F, 1F, 99F}; + column1.IsIn(0F, 1F, 99F); + VerifyNonStaticCall(column1, "isin", expected); + } + { + Column column1 = CreateColumn("col1"); + var expected = new List(){0.0, 1.0, 99.99}; + column1.IsIn(0.0, 1.0, 99.99); + VerifyNonStaticCall(column1, "isin", expected); + } + { + Column column1 = CreateColumn("col1"); + decimal decimal1 = 1; + decimal decimal2 = 2; + decimal decimal3 = 3; + + var expected = new List(){decimal1, decimal2, decimal3}; + column1.IsIn(decimal1, decimal2, decimal3); + VerifyNonStaticCall(column1, "isin", expected); + } + } + + private void VerifyNonStaticCall( + IJvmObjectReferenceProvider obj, + string methodName, + object arg0) + { + _mockJvm.Verify(m => m.CallNonStaticJavaMethod( + obj.Reference, + methodName, + arg0)); + } + + private void VerifyNonStaticCall( + IJvmObjectReferenceProvider obj, + string methodName, + object arg0, + object arg1) + { + _mockJvm.Verify(m => m.CallNonStaticJavaMethod( + obj.Reference, + methodName, + arg0, arg1)); + } + + private void VerifyNonStaticCall( + IJvmObjectReferenceProvider obj, + string methodName, + params object[] args) + { + _mockJvm.Verify(m => m.CallNonStaticJavaMethod( + obj.Reference, + methodName, + args)); + } + + [Theory] + [InlineData("EqNullSafe", "eqNullSafe")] + [InlineData("Or", "or")] + [InlineData("And", "and")] + [InlineData("Contains", "contains")] + [InlineData("StartsWith", "startsWith")] + [InlineData("EndsWith", "endsWith")] + [InlineData("EqualTo", "equalTo")] + [InlineData("NotEqual", "notEqual")] + [InlineData("Gt", "gt")] + [InlineData("Lt", "lt")] + [InlineData("Leq", "leq")] + [InlineData("Geq", "geq")] + [InlineData("Otherwise", "otherwise")] + [InlineData("Plus", "plus")] + [InlineData("Minus", "minus")] + [InlineData("Multiply", "multiply")] + [InlineData("Divide", "divide")] + [InlineData("Mod", "mod")] + [InlineData("GetItem", "getItem")] + [InlineData("BitwiseOR", "bitwiseOR")] + [InlineData("BitwiseAND", "bitwiseAND")] + [InlineData("BitwiseXOR", "bitwiseXOR")] + public void TestNamedOperators(string funcName, string opName) + { + Column column1 = CreateColumn("col1"); + Column column2 = CreateColumn("col2"); + System.Reflection.MethodInfo func = column1.GetType().GetMethod( + funcName, + new Type[] { typeof(Column) }); + var result = func.Invoke(column1, new[] { column2 }) as Column; + VerifyNonStaticCall(column1, opName, column2); + Assert.Equal("result", GetId(result)); + } + + [Theory] + [InlineData("Contains", "contains")] + [InlineData("StartsWith", "startsWith")] + [InlineData("EndsWith", "endsWith")] + [InlineData("Alias", "alias")] + [InlineData("As", "alias")] + [InlineData("Name", "name")] + [InlineData("Cast", "cast")] + [InlineData("Otherwise", "otherwise")] + [InlineData("Like", "like")] + [InlineData("RLike", "rlike")] + [InlineData("GetItem", "getItem")] + [InlineData("GetField", "getField")] + public void TestNamedOperatorsWithString(string funcName, string opName) + { + // These operators take string as the operand. + Column column = CreateColumn("col"); + string literal = "hello"; + System.Reflection.MethodInfo func = column.GetType().GetMethod( + funcName, + new Type[] { typeof(string) }); + var result = func.Invoke(column, new[] { literal }) as Column; + Assert.Equal("result", GetId(result)); + VerifyNonStaticCall(column, opName, literal); + } + + [Theory] + [InlineData("Asc", "asc")] + [InlineData("AscNullsFirst", "asc_nulls_first")] + [InlineData("AscNullsLast", "asc_nulls_last")] + [InlineData("Desc", "desc")] + [InlineData("DescNullsFirst", "desc_nulls_first")] + [InlineData("DescNullsLast", "desc_nulls_last")] + [InlineData("IsNaN", "isNaN")] + [InlineData("IsNull", "isNull")] + [InlineData("IsNotNull", "isNotNull")] + public void TestUnaryOperators(string funcName, string opName) + { + Column column = CreateColumn("col"); + + // Use an empty array of Type objects to get a method that takes no parameters. + System.Reflection.MethodInfo func = + column.GetType().GetMethod(funcName, Type.EmptyTypes); + var result = func.Invoke(column, null) as Column; + Assert.Equal("result", GetId(result)); + VerifyNonStaticCall(column, opName); + } + + private Column CreateColumn(string id) + { + return new Column(new JvmObjectReference(id, _mockJvm.Object)); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/Sql/RowTests.cs b/tests/Flowthru.Tests.Spark/Sql/RowTests.cs new file mode 100644 index 00000000..b3053eb0 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/Sql/RowTests.cs @@ -0,0 +1,164 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Network; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; +using Flowthru.Tests.Spark.TestUtils; +using Flowthru.Spark.Utils; +using Moq; +using Razorvine.Pickle; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + public class RowTests + { + private readonly string _testJsonSchema = + @"{ + ""type"":""struct"", + ""fields"":[ + { + ""name"":""age"", + ""type"":""integer"", + ""nullable"":true, + ""metadata"":{} + }, + { + ""name"":""name"", + ""type"":""string"", + ""nullable"":false, + ""metadata"":{} + } + ]}"; + + [Fact] + public void RowTest() + { + var structFields = new List() + { + new StructField("col1", new IntegerType()), + new StructField("col2", new StringType()), + }; + + var schema = new StructType(structFields); + + var row = new Row(new object[] { 1, "abc" }, schema); + + // Validate Size(). + Assert.Equal(2, row.Size()); + + // Validate [] operator. + Assert.Equal(1, row[0]); + Assert.Equal("abc", row[1]); + + // Validate Get*(int). + Assert.Equal(1, row.Get(0)); + Assert.Equal("abc", row.Get(1)); + Assert.Equal(1, row.GetAs(0)); + Assert.ThrowsAny(() => row.GetAs(0)); + Assert.Equal("abc", row.GetAs(1)); + Assert.ThrowsAny(() => row.GetAs(1)); + + // Validate Get*(string). + Assert.Equal(1, row.Get("col1")); + Assert.Equal("abc", row.Get("col2")); + Assert.Equal(1, row.GetAs("col1")); + Assert.ThrowsAny(() => row.GetAs("col1")); + Assert.Equal("abc", row.GetAs("col2")); + Assert.ThrowsAny(() => row.GetAs("col2")); + } + + [Fact] + public void RowConstructorTest() + { + Pickler pickler = CreatePickler(); + + var schema = (StructType)DataType.ParseDataType(_testJsonSchema); + var row1 = new Row(new object[] { 10, "name1" }, schema); + var row2 = new Row(new object[] { 15, "name2" }, schema); + byte[] pickledBytes = pickler.dumps(new[] { row1, row2 }); + + // Note that the following will invoke RowConstructor.construct(). + object[] unpickledData = PythonSerDe.GetUnpickledObjects( + new MemoryStream(pickledBytes), + pickledBytes.Length); + + Assert.Equal(2, unpickledData.Length); + Assert.Equal(row1, unpickledData[0]); + Assert.Equal(row2, unpickledData[1]); + } + + [Fact] + public void RowCollectorTest() + { + var stream = new MemoryStream(); + Pickler pickler = CreatePickler(); + + var schema = (StructType)DataType.ParseDataType(_testJsonSchema); + + // Pickle two rows in one batch. + var row1 = new Row(new object[] { 10, "name1" }, schema); + var row2 = new Row(new object[] { 15, "name2" }, schema); + byte[] batch1 = pickler.dumps(new[] { row1, row2 }); + SerDe.Write(stream, batch1.Length); + SerDe.Write(stream, batch1); + + // Pickle one row in one batch. + var row3 = new Row(new object[] { 20, "name3" }, schema); + byte[] batch2 = pickler.dumps(new[] { row3 }); + SerDe.Write(stream, batch2.Length); + SerDe.Write(stream, batch2); + + // Rewind the memory stream so that the row collect can read from beginning. + stream.Seek(0, SeekOrigin.Begin); + + // Set up the mock to return memory stream to which pickled data is written. + var socket = new Mock(); + socket.Setup(m => m.InputStream).Returns(stream); + socket.Setup(m => m.OutputStream).Returns(stream); + + var rowCollector = new RowCollector(); + Row[] rows = rowCollector.Collect(socket.Object).ToArray(); + + Assert.Equal(3, rows.Length); + Assert.Equal(row1, rows[0]); + Assert.Equal(row2, rows[1]); + Assert.Equal(row3, rows[2]); + } + + private Pickler CreatePickler() + { + new StructTypePickler().Register(); + new TestUtils.RowPickler().Register(); + return new Pickler(); + } + + [Fact] + public void GenericRowTest() + { + var row = new GenericRow(new object[] { 1, "abc" }); + + // Validate Size(). + Assert.Equal(2, row.Size()); + + // Validate [] operator. + Assert.Equal(1, row[0]); + Assert.Equal("abc", row[1]); + + // Validate Get*(int). + Assert.Equal(1, row.Get(0)); + Assert.Equal("abc", row.Get(1)); + Assert.Equal(1, row.GetAs(0)); + Assert.ThrowsAny(() => row.GetAs(0)); + Assert.Equal("abc", row.GetAs(1)); + Assert.ThrowsAny(() => row.GetAs(1)); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/Sql/TimestampTests.cs b/tests/Flowthru.Tests.Spark/Sql/TimestampTests.cs new file mode 100644 index 00000000..a338352b --- /dev/null +++ b/tests/Flowthru.Tests.Spark/Sql/TimestampTests.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Flowthru.Spark.Sql.Types; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + public class TimestampTests + { + [Fact] + public void TimestampTest() + { + { + var testDate = + new DateTime(2020, 1, 1, 8, 30, 30, DateTimeKind.Utc).AddTicks(1230); + var timestamp = new Timestamp(testDate); + + // Validate values. + Assert.Equal(2020, timestamp.Year); + Assert.Equal(1, timestamp.Month); + Assert.Equal(1, timestamp.Day); + Assert.Equal(8, timestamp.Hour); + Assert.Equal(30, timestamp.Minute); + Assert.Equal(30, timestamp.Second); + Assert.Equal(123, timestamp.Microsecond); + Assert.Equal(DateTimeKind.Utc, timestamp.ToDateTime().Kind); + + // Validate ToString(). + Assert.Equal("2020-01-01 08:30:30.000123Z", timestamp.ToString()); + + // Validate ToDateTime(). + Assert.Equal(testDate, timestamp.ToDateTime()); + } + + { + // Validate TimeZone. + var timestamp = new Timestamp( + new DateTime(2020, 1, 1, 8, 30, 30, DateTimeKind.Local)); + + Assert.Equal(DateTimeKind.Utc, timestamp.ToDateTime().Kind); + } + + { + var timestamp = new Timestamp(2020, 1, 2, 15, 30, 30, 123456); + + // Validate values. + Assert.Equal(2020, timestamp.Year); + Assert.Equal(1, timestamp.Month); + Assert.Equal(2, timestamp.Day); + Assert.Equal(15, timestamp.Hour); + Assert.Equal(30, timestamp.Minute); + Assert.Equal(30, timestamp.Second); + Assert.Equal(123456, timestamp.Microsecond); + Assert.Equal(DateTimeKind.Utc, timestamp.ToDateTime().Kind); + + // Validate ToString(). + Assert.Equal("2020-01-02 15:30:30.123456Z", timestamp.ToString()); + + // Validate ToDateTime(). + Assert.Equal( + new DateTime(2020, 1, 2, 15, 30, 30, DateTimeKind.Utc).AddTicks(1234560), + timestamp.ToDateTime()); + } + + { + // Validate microsecond values. + Assert.Throws( + () => new Timestamp(2020, 1, 2, 15, 30, 30, 1234567)); + } + } + + [Fact] + public void TestTimestampToString() + { + var dateTimeObj = new DateTime(2021, 01, 01); + Assert.Equal( + new Timestamp(DateTime.Parse(new Timestamp(dateTimeObj).ToString())), + new Timestamp(dateTimeObj)); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/Sql/TypesTests.cs b/tests/Flowthru.Tests.Spark/Sql/TypesTests.cs new file mode 100644 index 00000000..6884988d --- /dev/null +++ b/tests/Flowthru.Tests.Spark/Sql/TypesTests.cs @@ -0,0 +1,165 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Flowthru.Spark.Sql.Types; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + public class TypesTests + { + [Theory] + [InlineData("null")] + [InlineData("string")] + [InlineData("binary")] + [InlineData("boolean")] + [InlineData("date")] + [InlineData("timestamp")] + [InlineData("double")] + [InlineData("float")] + [InlineData("byte")] + [InlineData("integer")] + [InlineData("long")] + [InlineData("short")] + public void TestSimpleTypes(string typeName) + { + DataType atomicType = DataType.ParseDataType($@"""{typeName}"""); + Assert.Equal(typeName, atomicType.TypeName); + Assert.Equal(typeName, atomicType.SimpleString); + } + + [Fact] + public void TestArrayType() + { + string schemaJson = + @"{ + ""type"":""array"", + ""elementType"":""integer"", + ""containsNull"":false + }"; + var arrayType = (ArrayType)DataType.ParseDataType(schemaJson); + Assert.Equal("array", arrayType.TypeName); + Assert.Equal("array", arrayType.SimpleString); + Assert.Equal("integer", arrayType.ElementType.TypeName); + Assert.False(arrayType.ContainsNull); + } + + [Fact] + public void TestArrayTypeFromInternal() + { + { + var arrayType = new ArrayType(new IntegerType()); + Assert.False(arrayType.NeedConversion()); + + var expected = new ArrayList(Enumerable.Range(0, 10).ToArray()); + var actual = (ArrayList)arrayType.FromInternal(expected); + Assert.Same(expected, actual); + } + { + var dateType = new DateType(); + var arrayType = new ArrayType(dateType); + Assert.True(arrayType.NeedConversion()); + + var internalDates = new int[] { 10, 100 }; + Date[] expected = + internalDates.Select(i => (Date)dateType.FromInternal(i)).ToArray(); + var actual = (ArrayList)arrayType.FromInternal(new ArrayList(internalDates)); + Assert.Equal(expected, actual.ToArray()); + } + } + + [Fact] + public void TestMapType() + { + string schemaJson = + @"{ + ""type"":""map"", + ""keyType"":""integer"", + ""valueType"":""double"", + ""valueContainsNull"":false + }"; + var mapType = (MapType)DataType.ParseDataType(schemaJson); + Assert.Equal("map", mapType.TypeName); + Assert.Equal("map", mapType.SimpleString); + Assert.Equal("integer", mapType.KeyType.TypeName); + Assert.Equal("double", mapType.ValueType.TypeName); + Assert.False(mapType.ValueContainsNull); + } + + [Fact] + public void TestMapTypeFromInternal() + { + { + var integerType = new IntegerType(); + var mapType = new MapType(integerType, integerType); + Assert.False(mapType.NeedConversion()); + + Dictionary dict = + Enumerable.Range(0, 10).ToDictionary(i => i, i => i * i); + var expected = new Hashtable(dict); + var actual = (Hashtable)mapType.FromInternal(expected); + Assert.Same(expected, actual); + } + { + var integerType = new IntegerType(); + var dateType = new DateType(); + var mapType = new MapType(integerType, dateType); + Assert.True(mapType.NeedConversion()); + + var internalDates = new int[] { 10, 100 }; + var expected = new Hashtable( + internalDates.ToDictionary(i => i, i => (Date)dateType.FromInternal(i))); + var actual = (Hashtable)mapType.FromInternal( + new Hashtable(internalDates.ToDictionary(i => i, i => i))); + Assert.Equal(expected, actual); + } + } + + [Fact] + public void TestStructTypeAndStructFieldTypes() + { + string schemaJson = + @"{ + ""type"":""struct"", + ""fields"":[ + { + ""name"":""age"", + ""type"":""long"", + ""nullable"":true, + ""metadata"":{} + }, + { + ""name"":""name"", + ""type"":""string"", + ""nullable"":false, + ""metadata"":{} + } + ]}"; + + var structType = (StructType)DataType.ParseDataType(schemaJson); + Assert.Equal("struct", structType.TypeName); + Assert.Equal("struct", structType.SimpleString); + Assert.Equal(2, structType.Fields.Count); + + { + StructField field = structType.Fields[0]; + Assert.Equal("age", field.Name); + Assert.Equal("long", field.DataType.TypeName); + Assert.True(field.IsNullable); + Assert.Equal(new JObject(), field.Metadata); + } + { + StructField field = structType.Fields[1]; + Assert.Equal("name", field.Name); + Assert.Equal("string", field.DataType.TypeName); + Assert.False(field.IsNullable); + Assert.Equal(new JObject(), field.Metadata); + } + } + } +} diff --git a/tests/Flowthru.Tests.Spark/TestUtils/ArrowTestUtils.cs b/tests/Flowthru.Tests.Spark/TestUtils/ArrowTestUtils.cs new file mode 100644 index 00000000..effa9ba1 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/TestUtils/ArrowTestUtils.cs @@ -0,0 +1,200 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; +using Apache.Arrow; +using Apache.Arrow.Types; +using Microsoft.Data.Analysis; +using Xunit; + +namespace Flowthru.Tests.Spark.TestUtils +{ + public static class ArrowTestUtils + { + public static void AssertEquals(string expectedValue, IArrowArray arrowArray) + { + Assert.IsType(arrowArray); + var stringArray = (StringArray)arrowArray; + Assert.Equal(1, stringArray.Length); + Assert.Equal(expectedValue, stringArray.GetString(0)); + } + + public static void AssertEquals(string expectedValue, DataFrameColumn arrowArray) + { + var stringArray = (ArrowStringDataFrameColumn)arrowArray; + Assert.Equal(1, stringArray.Length); + Assert.Equal(expectedValue, stringArray[0]); + } + + public static IArrowType GetArrowType() + { + Type type = typeof(T); + return type switch + { + _ when type == typeof(bool) => BooleanType.Default, + _ when type == typeof(sbyte) => Int8Type.Default, + _ when type == typeof(byte) => UInt8Type.Default, + _ when type == typeof(short) => Int16Type.Default, + _ when type == typeof(ushort) => UInt16Type.Default, + _ when type == typeof(int) => Int32Type.Default, + _ when type == typeof(uint) => UInt32Type.Default, + _ when type == typeof(long) => Int64Type.Default, + _ when type == typeof(ulong) => UInt64Type.Default, + _ when type == typeof(float) => FloatType.Default, + _ when type == typeof(double) => DoubleType.Default, + _ when type == typeof(DateTime) => Date64Type.Default, + _ when type == typeof(TimeSpan) => TimestampType.Default, + _ when type == typeof(string) => StringType.Default, + _ when type == typeof(byte[]) => BinaryType.Default, + _ => throw new NotSupportedException($"Unknown type: {typeof(T)}") + }; + } + + public static ArrowStringDataFrameColumn ToArrowStringDataFrameColumn(StringArray array) + { + return new ArrowStringDataFrameColumn("String", + array.ValueBuffer.Memory, + array.ValueOffsetsBuffer.Memory, + array.NullBitmapBuffer.Memory, + array.Length, + array.NullCount); + } + + public static IArrowArray ToArrowArray(T[] array) + { + Type type = typeof(T); + object arrayObject = array; + return type switch + { + _ when type == typeof(bool) => ToBooleanArray((bool[])arrayObject), + _ when type == typeof(sbyte) => ToPrimitiveArrowArray((sbyte[])arrayObject), + _ when type == typeof(byte) => ToPrimitiveArrowArray((byte[])arrayObject), + _ when type == typeof(short) => ToPrimitiveArrowArray((short[])arrayObject), + _ when type == typeof(ushort) => ToPrimitiveArrowArray((ushort[])arrayObject), + _ when type == typeof(int) => ToPrimitiveArrowArray((int[])arrayObject), + _ when type == typeof(uint) => ToPrimitiveArrowArray((uint[])arrayObject), + _ when type == typeof(long) => ToPrimitiveArrowArray((long[])arrayObject), + _ when type == typeof(ulong) => ToPrimitiveArrowArray((ulong[])arrayObject), + _ when type == typeof(float) => ToPrimitiveArrowArray((float[])arrayObject), + _ when type == typeof(double) => ToPrimitiveArrowArray((double[])arrayObject), + _ when type == typeof(DateTime) => ToPrimitiveArrowArray((DateTime[])arrayObject), + _ when type == typeof(TimeSpan) => ToPrimitiveArrowArray((TimeSpan[])arrayObject), + _ when type == typeof(string) => ToStringArrowArray((string[])arrayObject), + _ when type == typeof(byte[]) => ToBinaryArrowArray((byte[][])arrayObject), + _ => throw new NotSupportedException($"Unknown type: {typeof(T)}") + }; + } + + public static IArrowArray ToPrimitiveArrowArray(T[] array) where T : struct + { + var builder = new ArrowBuffer.Builder(array.Length); + + // TODO: The builder should have an API for blitting an array, or its IEnumerable + // AppendRange should special-case T[] to do that directly when possible. + foreach (T item in array) + { + builder.Append(item); + } + + var data = new ArrayData( + GetArrowType(), + array.Length, + 0, + 0, + new[] { ArrowBuffer.Empty, builder.Build() }); + + return ArrowArrayFactory.BuildArray(data); + } + + private static IArrowArray ToBooleanArray(bool[] array) + { + byte[] rawBytes = CreateRawBytesForBoolArray(array.Length); + for (int i = 0; i < array.Length; ++i) + { + // only need to set true values since rawBytes is zeroed + // by the .NET runtime. + if (array[i]) + { + BitUtility.SetBit(rawBytes, i); + } + } + + var builder = new ArrowBuffer.Builder(rawBytes.Length); + builder.AppendRange(rawBytes); + + var data = new ArrayData( + BooleanType.Default, + array.Length, + 0, + 0, + new[] { ArrowBuffer.Empty, builder.Build() }); + + return ArrowArrayFactory.BuildArray(data); + } + + private static byte[] CreateRawBytesForBoolArray(int boolArrayLength) + { + int byteLength = boolArrayLength / 8; + if (boolArrayLength % 8 != 0) + { + ++byteLength; + } + + return new byte[byteLength]; + } + + private static IArrowArray ToStringArrowArray(string[] array) + { + var valueOffsets = new ArrowBuffer.Builder(); + var valueBuffer = new ArrowBuffer.Builder(); + int offset = 0; + + // TODO: Use array pool and encode directly into the array. + foreach (string str in array) + { + byte[] bytes = Encoding.UTF8.GetBytes(str); + valueOffsets.Append(offset); + // TODO: Anyway to use the span-based GetBytes to write directly to + // the value buffer? + valueBuffer.Append(bytes); + offset += bytes.Length; + } + + valueOffsets.Append(offset); + return new StringArray( + new ArrayData( + StringType.Default, + valueOffsets.Length - 1, + 0, + 0, + new[] { ArrowBuffer.Empty, valueOffsets.Build(), valueBuffer.Build() })); + } + + private static IArrowArray ToBinaryArrowArray(byte[][] array) + { + var valueOffsets = new ArrowBuffer.Builder(); + var valueBuffer = new ArrowBuffer.Builder(); + int offset = 0; + + foreach (byte[] bytes in array) + { + valueOffsets.Append(offset); + // TODO: Anyway to use the span-based GetBytes to write directly to + // the value buffer? + valueBuffer.Append(bytes); + offset += bytes.Length; + } + + valueOffsets.Append(offset); + return new StringArray( + new ArrayData( + BinaryType.Default, + valueOffsets.Length - 1, + 0, + 0, + new[] { ArrowBuffer.Empty, valueOffsets.Build(), valueBuffer.Build() })); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/TestUtils/RowPickler.cs b/tests/Flowthru.Tests.Spark/TestUtils/RowPickler.cs new file mode 100644 index 00000000..7bcf90ad --- /dev/null +++ b/tests/Flowthru.Tests.Spark/TestUtils/RowPickler.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Text; +using Flowthru.Spark.Interop.Ipc; +using Flowthru.Spark.Sql; +using Flowthru.Spark.Sql.Types; +using Razorvine.Pickle; + +namespace Flowthru.Tests.Spark.TestUtils +{ + /// + /// Custom pickler for StructType objects. + /// Refer to + /// spark/sql/core/src/main/scala/org/apache/spark/sql/execution/python/EvaluatePython.scala + /// + internal class StructTypePickler : IObjectPickler + { + private readonly string _module = "pyspark.sql.types"; + + public void Register() + { + Pickler.registerCustomPickler(GetType(), this); + Pickler.registerCustomPickler(typeof(StructType), this); + } + + public void pickle(object o, Stream stream, Pickler currentPickler) + { + if (!(o is StructType schema)) + { + throw new InvalidOperationException("A StructType object is expected."); + } + + SerDe.Write(stream, Opcodes.GLOBAL); + SerDe.Write(stream, Encoding.UTF8.GetBytes( + $"{_module}\n_parse_datatype_json_string\n")); + currentPickler.save(schema.Json); + SerDe.Write(stream, Opcodes.TUPLE1); + SerDe.Write(stream, Opcodes.REDUCE); + } + } + + /// + /// Custom pickler for Row objects. + /// Refer to + /// spark/sql/core/src/main/scala/org/apache/spark/sql/execution/python/EvaluatePython.scala + /// + internal class RowPickler : IObjectPickler + { + private readonly string _module = "pyspark.sql.types"; + + public void Register() + { + Pickler.registerCustomPickler(GetType(), this); + Pickler.registerCustomPickler(typeof(Row), this); + } + + public void pickle(object o, Stream stream, Pickler currentPickler) + { + if (o.Equals(this)) + { + SerDe.Write(stream, Opcodes.GLOBAL); + SerDe.Write(stream, Encoding.UTF8.GetBytes( + $"{_module}\n_create_row_inbound_converter\n")); + } + else + { + if (!(o is Row row)) + { + throw new InvalidOperationException("A Row object is expected."); + } + + currentPickler.save(this); + currentPickler.save(row.Schema); + SerDe.Write(stream, Opcodes.TUPLE1); + SerDe.Write(stream, Opcodes.REDUCE); + + SerDe.Write(stream, Opcodes.MARK); + for (int i = 0; i < row.Size(); ++i) + { + currentPickler.save(row.Get(i)); + } + + SerDe.Write(stream, Opcodes.TUPLE); + SerDe.Write(stream, Opcodes.REDUCE); + } + } + } +} diff --git a/tests/Flowthru.Tests.Spark/TestUtils/TemporaryDirectory.cs b/tests/Flowthru.Tests.Spark/TestUtils/TemporaryDirectory.cs new file mode 100644 index 00000000..1bf0bd55 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/TestUtils/TemporaryDirectory.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; + +namespace Flowthru.Tests.Spark.TestUtils +{ + /// + /// Creates a temporary folder that is automatically cleaned up when disposed. + /// + internal sealed class TemporaryDirectory : IDisposable + { + private bool _disposed = false; + + /// + /// Path to temporary folder. + /// + public string Path { get; } + + public TemporaryDirectory() + { + Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString()); + Cleanup(); + Directory.CreateDirectory(Path); + Path = $"{Path}{System.IO.Path.DirectorySeparatorChar}"; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Cleanup() + { + if (File.Exists(Path)) + { + File.Delete(Path); + } + else if (Directory.Exists(Path)) + { + Directory.Delete(Path, true); + } + } + + private void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + Cleanup(); + } + + _disposed = true; + } + } +} diff --git a/tests/Flowthru.Tests.Spark/TestUtils/XunitConsoleOutHelper.cs b/tests/Flowthru.Tests.Spark/TestUtils/XunitConsoleOutHelper.cs new file mode 100644 index 00000000..47912278 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/TestUtils/XunitConsoleOutHelper.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Xunit.Abstractions; + +namespace Flowthru.Tests.Spark.TestUtils +{ + // Tests can subclass this to get Console output to display when using + // xUnit testing framework. + // Workaround found at https://github.com/microsoft/vstest/issues/799 + public class XunitConsoleOutHelper : IDisposable + { + private readonly ITestOutputHelper _output; + private readonly TextWriter _originalOut; + private readonly TextWriter _textWriter; + + public XunitConsoleOutHelper(ITestOutputHelper output) + { + _output = output; + _originalOut = Console.Out; + _textWriter = new StringWriter(); + Console.SetOut(_textWriter); + } + + public void Dispose() + { + _output.WriteLine(_textWriter.ToString()); + Console.SetOut(_originalOut); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/TypeConverterTests.cs b/tests/Flowthru.Tests.Spark/TypeConverterTests.cs new file mode 100644 index 00000000..6cd3d238 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/TypeConverterTests.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Flowthru.Spark.Utils; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + public class TypeConverterTests + { + [Fact] + public void TestBaseCase() + { + Assert.Equal((byte)0x01, TypeConverter.ConvertTo((byte)0x01)); + Assert.Equal((sbyte)0x01, TypeConverter.ConvertTo((sbyte)0x01)); + Assert.Equal((short)1, TypeConverter.ConvertTo((short)1)); + Assert.Equal((ushort)1, TypeConverter.ConvertTo((ushort)1)); + Assert.Equal(1, TypeConverter.ConvertTo(1)); + Assert.Equal(1L, TypeConverter.ConvertTo(1)); + Assert.Equal(1u, TypeConverter.ConvertTo(1u)); + Assert.Equal(1L, TypeConverter.ConvertTo(1L)); + Assert.Equal(1ul, TypeConverter.ConvertTo(1ul)); + Assert.Equal(1.0f, TypeConverter.ConvertTo(1.0f)); + Assert.Equal(1.0d, TypeConverter.ConvertTo(1.0d)); + Assert.Equal(1.0m, TypeConverter.ConvertTo(1.0m)); + Assert.Equal('a', TypeConverter.ConvertTo('a')); + Assert.Equal("test", TypeConverter.ConvertTo("test")); + Assert.True(TypeConverter.ConvertTo(true)); + } + + [Fact] + public void TestArrayList() + { + var expected = new ArrayList(Enumerable.Range(0, 10).ToArray()); + ArrayList actual = TypeConverter.ConvertTo(expected); + Assert.Same(expected, actual); + } + + [Fact] + public void TestArray() + { + int[] expected = Enumerable.Range(0, 10).ToArray(); + var arrayList = new ArrayList(expected); + int[] actual = TypeConverter.ConvertTo(arrayList); + Assert.Equal(expected, actual); + } + + [Fact] + public void TestArrayArray() + { + int[][] expected = + Enumerable.Range(0, 10).Select(i => Enumerable.Range(i, 10).ToArray()).ToArray(); + + var arrayList = new ArrayList(expected.Length); + for (int i = 0; i < expected.Length; ++i) + { + int[] innerExpected = expected[i]; + var innerArrayList = new ArrayList(innerExpected.Length); + for (int j = 0; j < innerExpected.Length; ++j) + { + innerArrayList.Add(innerExpected[j]); + } + + arrayList.Add(innerArrayList); + } + + int[][] actual = TypeConverter.ConvertTo(arrayList); + Assert.Equal(expected, actual); + } + + [Fact] + public void TestArrayArrayArray() + { + int[][][] expected = Enumerable.Range(0, 10) + .Select(i => Enumerable.Range(i, 10)) + .Select(arr => arr.Select(j => Enumerable.Range(j, 10).ToArray()).ToArray()) + .ToArray(); + + var arrayList = new ArrayList(expected.Length); + for (int i = 0; i < expected.Length; ++i) + { + int[][] innerExpected = expected[i]; + var innerArrayList = new ArrayList(innerExpected.Length); + for (int j = 0; j < innerExpected.Length; ++j) + { + int[] innerInnerExpected = expected[i][j]; + var innerInnerArrayList = new ArrayList(innerInnerExpected.Length); + for (int k = 0; k < innerInnerExpected.Length; ++k) + { + innerInnerArrayList.Add(innerInnerExpected[k]); + } + + innerArrayList.Add(innerInnerArrayList); + } + + arrayList.Add(innerArrayList); + } + + int[][][] actual = TypeConverter.ConvertTo(arrayList); + Assert.Equal(expected, actual); + } + + [Fact] + public void TestHashtable() + { + var expected = + new Hashtable(Enumerable.Range(0, 10).ToDictionary(k => k, v => v * v)); + Hashtable actual = TypeConverter.ConvertTo(expected); + Assert.Same(expected, actual); + } + + [Fact] + public void TestDictionary() + { + Dictionary expected = + Enumerable.Range(0, 10).ToDictionary(i => i, i => i * i); + var hashtable = new Hashtable(expected); + Dictionary actual = TypeConverter.ConvertTo>(hashtable); + Assert.Equal(expected, actual); + } + + [Fact] + public void TestDictionaryDictionary() + { + Dictionary> expected = Enumerable + .Range(0, 10) + .ToDictionary( + i => i, + i => Enumerable.Range(i, 10).ToDictionary(j => j, j => j * j)); + + var hashtable = new Hashtable(); + foreach (KeyValuePair> kvp in expected) + { + var innerHashtable = new Hashtable(); + foreach(KeyValuePair innerKvp in kvp.Value) + { + innerHashtable[innerKvp.Key] = innerKvp.Value; + } + + hashtable[kvp.Key] = innerHashtable; + } + + Dictionary> actual = + TypeConverter.ConvertTo>>(hashtable); + Assert.Equal(expected, actual); + } + + [Fact] + public void TestDictionaryAndArray() + { + { + Dictionary expected = Enumerable + .Range(0, 10) + .ToDictionary( + i => i, + i => Enumerable.Range(i, 10).ToArray()); + + var hashtable = new Hashtable(); + foreach (KeyValuePair kvp in expected) + { + var arrayList = new ArrayList(); + for (int i = 0; i < kvp.Value.Length; ++i) + { + arrayList.Add(kvp.Value[i]); + } + + hashtable[kvp.Key] = arrayList; + } + + Dictionary actual = + TypeConverter.ConvertTo>(hashtable); + Assert.Equal(expected, actual); + } + { + Dictionary[] expected = Enumerable + .Range(0, 10) + .Select(i => Enumerable.Range(i, 10).ToDictionary(j => j, j => j * j)) + .ToArray(); + + var arrayList = new ArrayList(); + for (int i = 0; i < expected.Length; ++i) + { + var hashtable = new Hashtable(); + foreach (KeyValuePair kvp in expected[i]) + { + hashtable[kvp.Key] = kvp.Value; + } + + arrayList.Add(hashtable); + } + + Dictionary[] actual = + TypeConverter.ConvertTo[]>(arrayList); + Assert.Equal(expected, actual); + } + } + } +} diff --git a/tests/Flowthru.Tests.Spark/UdfSerDeTests.cs b/tests/Flowthru.Tests.Spark/UdfSerDeTests.cs new file mode 100644 index 00000000..3878bc24 --- /dev/null +++ b/tests/Flowthru.Tests.Spark/UdfSerDeTests.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Reflection; +using Flowthru.Spark.Utils; +using Xunit; + +namespace Flowthru.Tests.Spark +{ + [Collection("Spark Unit Tests")] + public class UdfSerDeTests + { + [Serializable] + private class TestClass + { + private readonly string str; + + public TestClass(string str) + { + this.str = str; + } + + public string Concat(string s) + { + if (str == null) + { + return s + s; + } + + return str + s; + } + + public override bool Equals(object obj) + { + var that = obj as TestClass; + + if (that == null) + { + return false; + } + + return str == that.str; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } + + [Fact] + public void TestUdfSerDe() + { + { + // Without closure. + Func expectedUdf = i => 10 * i; + Delegate actualUdf = SerDe(expectedUdf); + + VerifyUdfSerDe(expectedUdf, actualUdf, false); + Assert.Equal(100, ((Func)actualUdf)(10)); + } + + { + // With closure where the delegate target is an anonymous class. + // The target will contain fields ["tc1", "tc2"], where "tc1" is + // non null and "tc2" is null. + TestClass tc1 = new TestClass("Test"); + TestClass tc2 = null; + Func expectedUdf = + (s) => + { + if (tc2 == null) + { + return tc1.Concat(s); + } + return s; + }; + Delegate actualUdf = SerDe(expectedUdf); + + VerifyUdfSerDe(expectedUdf, actualUdf, true); + Assert.Equal("TestHelloWorld", ((Func)actualUdf)("HelloWorld")); + } + + { + // With closure where the delegate target is TestClass + // and target's field "_str" is set to "Test". + TestClass tc = new TestClass("Test"); + Func expectedUdf = tc.Concat; + Delegate actualUdf = SerDe(expectedUdf); + + VerifyUdfSerDe(expectedUdf, actualUdf, true); + Assert.Equal("TestHelloWorld", ((Func)actualUdf)("HelloWorld")); + } + + { + // With closure where the delegate target is TestClass, + // and target's field "_str" is set to null. + TestClass tc = new TestClass(null); + Func expectedUdf = tc.Concat; + Delegate actualUdf = SerDe(expectedUdf); + + VerifyUdfSerDe(expectedUdf, actualUdf, true); + Assert.Equal( + "HelloWorldHelloWorld", + ((Func)actualUdf)("HelloWorld")); + } + } + + private void VerifyUdfSerDe(Delegate expectedUdf, Delegate actualUdf, bool hasClosure) + { + VerifyUdfData( + UdfSerDe.Serialize(expectedUdf), + UdfSerDe.Serialize(actualUdf), + hasClosure); + VerifyDelegate(expectedUdf, actualUdf); + } + + private void VerifyUdfData( + UdfSerDe.UdfData expectedUdfData, + UdfSerDe.UdfData actualUdfData, + bool hasClosure) + { + Assert.Equal(expectedUdfData, actualUdfData); + + if (!hasClosure) + { + Assert.Null(expectedUdfData.TargetData.Fields); + Assert.Null(actualUdfData.TargetData.Fields); + } + } + + private void VerifyDelegate(Delegate expectedDelegate, Delegate actualDelegate) + { + Assert.Equal(expectedDelegate.GetType(), actualDelegate.GetType()); + Assert.Equal(expectedDelegate.Method, actualDelegate.Method); + Assert.Equal(expectedDelegate.Target.GetType(), actualDelegate.Target.GetType()); + + FieldInfo[] expectedFields = expectedDelegate.Target.GetType().GetFields(); + FieldInfo[] actualFields = actualDelegate.Target.GetType().GetFields(); + Assert.Equal(expectedFields, actualFields); + } + + private Delegate SerDe(Delegate udf) + { + return Deserialize(Serialize(udf)); + } + + private byte[] Serialize(Delegate udf) + { + UdfSerDe.UdfData udfData = UdfSerDe.Serialize(udf); + + using (var ms = new MemoryStream()) + { + BinarySerDe.Serialize(ms, udfData); + return ms.ToArray(); + } + } + + private Delegate Deserialize(byte[] serializedUdf) + { + using (var ms = new MemoryStream(serializedUdf, false)) + { + var udfData = BinarySerDe.Deserialize(ms); + return UdfSerDe.Deserialize(udfData); + } + } + } +} diff --git a/tests/Flowthru.Tests.Spark/UdfWrapperTests.cs b/tests/Flowthru.Tests.Spark/UdfWrapperTests.cs new file mode 100644 index 00000000..75ce7bfc --- /dev/null +++ b/tests/Flowthru.Tests.Spark/UdfWrapperTests.cs @@ -0,0 +1,599 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Apache.Arrow; +using Flowthru.Spark.Sql; +using Flowthru.Tests.Spark.TestUtils; +using Microsoft.Data.Analysis; +using Xunit; +using static Flowthru.Tests.Spark.TestUtils.ArrowTestUtils; + +namespace Flowthru.Tests.Spark +{ + public class UdfWrapperTests + { + [Fact] + public void TestPicklingUdfWrapper0() + { + var udfWrapper = new PicklingUdfWrapper(() => 10); + Assert.Equal(10, udfWrapper.Execute(0, null, null)); + } + + [Fact] + public void TestPicklingUdfWrapper1() + { + var udfWrapper = new PicklingUdfWrapper((str1) => str1); + ValidatePicklingWrapper(1, udfWrapper); + } + + [Fact] + public void TestPicklingUdfWrapper2() + { + var udfWrapper = new PicklingUdfWrapper((str1, str2) => str1 + str2); + ValidatePicklingWrapper(2, udfWrapper); + } + + [Fact] + public void TestPicklingUdfWrapper3() + { + var udfWrapper = new PicklingUdfWrapper( + (str1, str2, str3) => str1 + str2 + str3 + ); + ValidatePicklingWrapper(3, udfWrapper); + } + + [Fact] + public void TestPicklingUdfWrapper4() + { + var udfWrapper = new PicklingUdfWrapper( + (str1, str2, str3, str4) => str1 + str2 + str3 + str4 + ); + ValidatePicklingWrapper(4, udfWrapper); + } + + [Fact] + public void TestPicklingUdfWrapper5() + { + var udfWrapper = new PicklingUdfWrapper( + (str1, str2, str3, str4, str5) => str1 + str2 + str3 + str4 + str5 + ); + ValidatePicklingWrapper(5, udfWrapper); + } + + [Fact] + public void TestPicklingUdfWrapper6() + { + var udfWrapper = new PicklingUdfWrapper< + string, + string, + string, + string, + string, + string, + string + >((str1, str2, str3, str4, str5, str6) => str1 + str2 + str3 + str4 + str5 + str6); + ValidatePicklingWrapper(6, udfWrapper); + } + + [Fact] + public void TestPicklingUdfWrapper7() + { + var udfWrapper = new PicklingUdfWrapper< + string, + string, + string, + string, + string, + string, + string, + string + >( + (str1, str2, str3, str4, str5, str6, str7) => str1 + str2 + str3 + str4 + str5 + str6 + str7 + ); + ValidatePicklingWrapper(7, udfWrapper); + } + + [Fact] + public void TestPicklingUdfWrapper8() + { + var udfWrapper = new PicklingUdfWrapper< + string, + string, + string, + string, + string, + string, + string, + string, + string + >( + (str1, str2, str3, str4, str5, str6, str7, str8) => + str1 + str2 + str3 + str4 + str5 + str6 + str7 + str8 + ); + ValidatePicklingWrapper(8, udfWrapper); + } + + [Fact] + public void TestPicklingUdfWrapper9() + { + var udfWrapper = new PicklingUdfWrapper< + string, + string, + string, + string, + string, + string, + string, + string, + string, + string + >( + (str1, str2, str3, str4, str5, str6, str7, str8, str9) => + str1 + str2 + str3 + str4 + str5 + str6 + str7 + str8 + str9 + ); + ValidatePicklingWrapper(9, udfWrapper); + } + + [Fact] + public void TestPicklingUdfWrapper10() + { + var udfWrapper = new PicklingUdfWrapper< + string, + string, + string, + string, + string, + string, + string, + string, + string, + string, + string + >( + (str1, str2, str3, str4, str5, str6, str7, str8, str9, str10) => + str1 + str2 + str3 + str4 + str5 + str6 + str7 + str8 + str9 + str10 + ); + ValidatePicklingWrapper(10, udfWrapper); + } + + // Validates the given udfWrapper, whose internal UDF concatenates all the input strings. + private void ValidatePicklingWrapper(int numArgs, dynamic udfWrapper) + { + // Create one more input data than the given numArgs to validate + // the indexing is working correctly inside UdfWrapper. + var input = new List(); + for (int i = 0; i < numArgs + 1; ++i) + { + input.Add($"arg{i}"); + } + + // First create argOffsets from 0 to numArgs. + // For example, the numArgs was 3, the expected strings is "arg0arg1arg2" + // where the argOffsets are created with { 0, 1, 2 }. + Assert.Equal( + string.Join("", input.GetRange(0, numArgs)), + udfWrapper.Execute(0, input.ToArray(), Enumerable.Range(0, numArgs).ToArray()) + ); + + // Create argOffsets from 1 to numArgs + 1. + // For example, the numArgs was 3, the expected strings is "arg1arg2arg3" + // where the argOffsets are created with { 1, 2, 3 }. + Assert.Equal( + string.Join("", input.GetRange(1, numArgs)), + udfWrapper.Execute(0, input.ToArray(), Enumerable.Range(1, numArgs).ToArray()) + ); + } + + [Fact] + public void TestArrowUdfWrapper1() + { + var udfWrapper = new ArrowUdfWrapper((str1) => str1); + ValidateArrowWrapper(1, udfWrapper); + } + + [Fact] + public void TestArrowUdfWrapper2() + { + var udfWrapper = new ArrowUdfWrapper( + (str1, str2) => Concat(str1, str2) + ); + ValidateArrowWrapper(2, udfWrapper); + } + + [Fact] + public void TestArrowUdfWrapper3() + { + var udfWrapper = new ArrowUdfWrapper( + (str1, str2, str3) => Concat(str1, str2, str3) + ); + ValidateArrowWrapper(3, udfWrapper); + } + + [Fact] + public void TestArrowUdfWrapper4() + { + var udfWrapper = new ArrowUdfWrapper< + StringArray, + StringArray, + StringArray, + StringArray, + StringArray + >((str1, str2, str3, str4) => Concat(str1, str2, str3, str4)); + ValidateArrowWrapper(4, udfWrapper); + } + + [Fact] + public void TestArrowUdfWrapper5() + { + var udfWrapper = new ArrowUdfWrapper< + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray + >((str1, str2, str3, str4, str5) => Concat(str1, str2, str3, str4, str5)); + ValidateArrowWrapper(5, udfWrapper); + } + + [Fact] + public void TestArrowUdfWrapper6() + { + var udfWrapper = new ArrowUdfWrapper< + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray + >((str1, str2, str3, str4, str5, str6) => Concat(str1, str2, str3, str4, str5, str6)); + ValidateArrowWrapper(6, udfWrapper); + } + + [Fact] + public void TestArrowUdfWrapper7() + { + var udfWrapper = new ArrowUdfWrapper< + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray + >( + (str1, str2, str3, str4, str5, str6, str7) => + Concat(str1, str2, str3, str4, str5, str6, str7) + ); + ValidateArrowWrapper(7, udfWrapper); + } + + [Fact] + public void TestArrowUdfWrapper8() + { + var udfWrapper = new ArrowUdfWrapper< + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray + >( + (str1, str2, str3, str4, str5, str6, str7, str8) => + Concat(str1, str2, str3, str4, str5, str6, str7, str8) + ); + ValidateArrowWrapper(8, udfWrapper); + } + + [Fact] + public void TestArrowUdfWrapper9() + { + var udfWrapper = new ArrowUdfWrapper< + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray + >( + (str1, str2, str3, str4, str5, str6, str7, str8, str9) => + Concat(str1, str2, str3, str4, str5, str6, str7, str8, str9) + ); + ValidateArrowWrapper(9, udfWrapper); + } + + [Fact] + public void TestArrowUdfWrapper10() + { + var udfWrapper = new ArrowUdfWrapper< + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray, + StringArray + >( + (str1, str2, str3, str4, str5, str6, str7, str8, str9, str10) => + Concat(str1, str2, str3, str4, str5, str6, str7, str8, str9, str10) + ); + ValidateArrowWrapper(10, udfWrapper); + } + + [Fact] + public void TestDataFrameUdfWrapper1() + { + var udfWrapper = new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >((str1) => str1); + ValidateDataFrameWrapper(1, udfWrapper); + } + + [Fact] + public void TestDataFrameUdfWrapper2() + { + var udfWrapper = new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >((str1, str2) => Concat(str1, str2)); + ValidateDataFrameWrapper(2, udfWrapper); + } + + [Fact] + public void TestDataFrameUdfWrapper3() + { + var udfWrapper = new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >((str1, str2, str3) => Concat(str1, str2, str3)); + ValidateDataFrameWrapper(3, udfWrapper); + } + + [Fact] + public void TestDataFrameUdfWrapper4() + { + var udfWrapper = new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >((str1, str2, str3, str4) => Concat(str1, str2, str3, str4)); + ValidateDataFrameWrapper(4, udfWrapper); + } + + [Fact] + public void TestDataFrameUdfWrapper5() + { + var udfWrapper = new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >((str1, str2, str3, str4, str5) => Concat(str1, str2, str3, str4, str5)); + ValidateDataFrameWrapper(5, udfWrapper); + } + + [Fact] + public void TestDataFrameUdfWrapper6() + { + var udfWrapper = new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >((str1, str2, str3, str4, str5, str6) => Concat(str1, str2, str3, str4, str5, str6)); + ValidateDataFrameWrapper(6, udfWrapper); + } + + [Fact] + public void TestDataFrameUdfWrapper7() + { + var udfWrapper = new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >( + (str1, str2, str3, str4, str5, str6, str7) => + Concat(str1, str2, str3, str4, str5, str6, str7) + ); + ValidateDataFrameWrapper(7, udfWrapper); + } + + [Fact] + public void TestDataFrameUdfWrapper8() + { + var udfWrapper = new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >( + (str1, str2, str3, str4, str5, str6, str7, str8) => + Concat(str1, str2, str3, str4, str5, str6, str7, str8) + ); + ValidateDataFrameWrapper(8, udfWrapper); + } + + [Fact] + public void TestDataFrameUdfWrapper9() + { + var udfWrapper = new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >( + (str1, str2, str3, str4, str5, str6, str7, str8, str9) => + Concat(str1, str2, str3, str4, str5, str6, str7, str8, str9) + ); + ValidateDataFrameWrapper(9, udfWrapper); + } + + [Fact] + public void TestDataFrameUdfWrapper10() + { + var udfWrapper = new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >( + (str1, str2, str3, str4, str5, str6, str7, str8, str9, str10) => + Concat(str1, str2, str3, str4, str5, str6, str7, str8, str9, str10) + ); + ValidateDataFrameWrapper(10, udfWrapper); + } + + private static StringArray Concat(params StringArray[] arrays) + { + var builder = new StringBuilder(); + int length = arrays[0].Length; + var resultStrings = new string[length]; + + for (int i = 0; i < length; ++i) + { + foreach (StringArray array in arrays) + { + builder.Append(array.GetString(i)); + } + + resultStrings[i] = builder.ToString(); + builder.Clear(); + } + + return (StringArray)ToArrowArray(resultStrings); + } + + private static ArrowStringDataFrameColumn Concat(params ArrowStringDataFrameColumn[] arrays) + { + var builder = new StringBuilder(); + var length = (int)arrays[0].Length; + var resultStrings = new string[length]; + + for (int i = 0; i < length; ++i) + { + foreach (ArrowStringDataFrameColumn array in arrays) + { + builder.Append(array[i]); + } + + resultStrings[i] = builder.ToString(); + builder.Clear(); + } + + var stringColumn = (StringArray)ToArrowArray(resultStrings); + return ToArrowStringDataFrameColumn(stringColumn); + } + + // Validates the given udfWrapper, whose internal UDF concatenates all the input strings. + private void ValidateArrowWrapper(int numArgs, dynamic udfWrapper) + { + // Create one more input data than the given numArgs to validate + // the indexing is working correctly inside ArrowUdfWrapper. + var input = new IArrowArray[numArgs + 1]; + var inputStrings = new List(); + for (int i = 0; i < input.Length; ++i) + { + inputStrings.Add($"arg{i}"); + input[i] = ToArrowArray(new string[] { $"arg{i}" }); + } + + // First create argOffsets from 0 to numArgs. + // For example, the numArgs was 3, the expected strings is "arg0arg1arg2" + // where the argOffsets are created with { 0, 1, 2 }. + ArrowTestUtils.AssertEquals( + string.Join("", inputStrings.GetRange(0, numArgs)), + udfWrapper.Execute(input, Enumerable.Range(0, numArgs).ToArray()) + ); + + // Create argOffsets from 1 to numArgs + 1. + // For example, the numArgs was 3, the expected strings is "arg1arg2arg3" + // where the argOffsets are created with { 1, 2, 3 }. + ArrowTestUtils.AssertEquals( + string.Join("", inputStrings.GetRange(1, numArgs)), + udfWrapper.Execute(input, Enumerable.Range(1, numArgs).ToArray()) + ); + } + + // Validates the given udfWrapper, whose internal UDF concatenates all the input strings. + private void ValidateDataFrameWrapper(int numArgs, dynamic udfWrapper) + { + // Create one more input data than the given numArgs to validate + // the indexing is working correctly inside ArrowUdfWrapper. + var input = new DataFrameColumn[numArgs + 1]; + var inputStrings = new List(); + for (int i = 0; i < input.Length; ++i) + { + inputStrings.Add($"arg{i}"); + var stringColumn = (StringArray)ToArrowArray(new string[] { $"arg{i}" }); + input[i] = ToArrowStringDataFrameColumn(stringColumn); + } + + // First create argOffsets from 0 to numArgs. + // For example, the numArgs was 3, the expected strings is "arg0arg1arg2" + // where the argOffsets are created with { 0, 1, 2 }. + ArrowTestUtils.AssertEquals( + string.Join("", inputStrings.GetRange(0, numArgs)), + udfWrapper.Execute(input, Enumerable.Range(0, numArgs).ToArray()) + ); + + // Create argOffsets from 1 to numArgs + 1. + // For example, the numArgs was 3, the expected strings is "arg1arg2arg3" + // where the argOffsets are created with { 1, 2, 3 }. + ArrowTestUtils.AssertEquals( + string.Join("", inputStrings.GetRange(1, numArgs)), + udfWrapper.Execute(input, Enumerable.Range(1, numArgs).ToArray()) + ); + } + } +} diff --git a/tests/Flowthru.Tests.Spark/WorkerFunctionTests.cs b/tests/Flowthru.Tests.Spark/WorkerFunctionTests.cs new file mode 100644 index 00000000..3ba4218e --- /dev/null +++ b/tests/Flowthru.Tests.Spark/WorkerFunctionTests.cs @@ -0,0 +1,350 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Apache.Arrow; +using Flowthru.Spark.Sql; +using Flowthru.Tests.Spark.TestUtils; +using Microsoft.Data.Analysis; +using Xunit; +using static Flowthru.Tests.Spark.TestUtils.ArrowTestUtils; + +namespace Flowthru.Tests.Spark +{ + public class WorkerFunctionTests + { + [Fact] + public void TestPicklingWorkerFunction() + { + var func = new PicklingWorkerFunction( + new PicklingUdfWrapper((str) => str).Execute + ); + + string[] input = { "arg1" }; + Assert.Equal(input[0], func.Func(0, input, new[] { 0 })); + } + + [Fact] + public void TestChainingPicklingWorkerFunction() + { + var func1 = new PicklingWorkerFunction( + new PicklingUdfWrapper((number, str) => $"{str}:{number}").Execute + ); + + var func2 = new PicklingWorkerFunction( + new PicklingUdfWrapper((str) => $"outer1:{str}").Execute + ); + + var func3 = new PicklingWorkerFunction( + new PicklingUdfWrapper((str) => $"outer2:{str}").Execute + ); + + object[] input = { 100, "name" }; + + // Validate one-level chaining. + PicklingWorkerFunction chainedFunc1 = PicklingWorkerFunction.Chain(func1, func2); + Assert.Equal("outer1:name:100", chainedFunc1.Func(0, input, new[] { 0, 1 })); + + // Validate two-level chaining. + PicklingWorkerFunction chainedFunc2 = PicklingWorkerFunction.Chain(chainedFunc1, func3); + Assert.Equal("outer2:outer1:name:100", chainedFunc2.Func(0, input, new[] { 0, 1 })); + } + + [Fact] + public void TestInvalidChainingPickling() + { + var func1 = new PicklingWorkerFunction( + new PicklingUdfWrapper((number, str) => $"{str}:{number}").Execute + ); + + var func2 = new PicklingWorkerFunction( + new PicklingUdfWrapper((str) => $"outer1:{str}").Execute + ); + + object[] input = { 100, "name" }; + + // The order does not align since workerFunction2 is executed first. + PicklingWorkerFunction chainedFunc1 = PicklingWorkerFunction.Chain(func2, func1); + Assert.ThrowsAny(() => chainedFunc1.Func(0, input, new[] { 0, 1 })); + } + + [Fact] + public void TestArrowWorkerFunction() + { + var func = new ArrowWorkerFunction( + new ArrowUdfWrapper((str) => str).Execute + ); + + string[] input = { "arg1" }; + ArrowTestUtils.AssertEquals(input[0], func.Func(new[] { ToArrowArray(input) }, new[] { 0 })); + } + + [Fact] + public void TestDataFrameWorkerFunction() + { + var func = new DataFrameWorkerFunction( + new DataFrameUdfWrapper( + (str) => str + ).Execute + ); + + string[] input = { "arg1" }; + var column = (StringArray)ToArrowArray(input); + ArrowStringDataFrameColumn ArrowStringDataFrameColumn = ToArrowStringDataFrameColumn(column); + ArrowTestUtils.AssertEquals( + input[0], + func.Func(new[] { ArrowStringDataFrameColumn }, new[] { 0 }) + ); + } + + /// + /// Tests the ArrowWorkerFunction handles boolean types correctly + /// for both input and output. + /// + [Fact] + public void TestArrowWorkerFunctionForBool() + { + var func = new ArrowWorkerFunction( + new ArrowUdfWrapper( + (strings, flags) => + (BooleanArray)ToArrowArray( + Enumerable + .Range(0, strings.Length) + .Select(i => flags.GetValue(i).Value || strings.GetString(i).Contains("true")) + .ToArray() + ) + ).Execute + ); + + IArrowArray[] input = new[] + { + ToArrowArray(new[] { "arg1_true", "arg1_true", "arg1_false", "arg1_false" }), + ToArrowArray(new[] { true, false, true, false }), + }; + var results = (BooleanArray)func.Func(input, new[] { 0, 1 }); + Assert.Equal(4, results.Length); + Assert.True(results.GetValue(0).Value); + Assert.True(results.GetValue(1).Value); + Assert.True(results.GetValue(2).Value); + Assert.False(results.GetValue(3).Value); + } + + /// + /// Tests the DataFrameWorkerFunction handles boolean types correctly + /// for both input and output. + /// + [Fact] + public void TestDataFrameWorkerFunctionForBool() + { + var func = new DataFrameWorkerFunction( + new DataFrameUdfWrapper< + ArrowStringDataFrameColumn, + BooleanDataFrameColumn, + BooleanDataFrameColumn + >( + (strings, flags) => + { + for (long i = 0; i < strings.Length; ++i) + { + flags[i] = flags[i].Value || strings[i].Contains("true"); + } + return flags; + } + ).Execute + ); + + var stringColumn = (StringArray)ToArrowArray( + new[] { "arg1_true", "arg1_true", "arg1_false", "arg1_false" } + ); + + ArrowStringDataFrameColumn ArrowStringDataFrameColumn = ToArrowStringDataFrameColumn( + stringColumn + ); + var boolColumn = new BooleanDataFrameColumn( + "Bool", + Enumerable.Range(0, 4).Select(x => x % 2 == 0) + ); + var input = new DataFrameColumn[] { ArrowStringDataFrameColumn, boolColumn }; + var results = (BooleanDataFrameColumn)func.Func(input, new[] { 0, 1 }); + Assert.Equal(4, results.Length); + Assert.True(results[0]); + Assert.True(results[1]); + Assert.True(results[2]); + Assert.False(results[3]); + } + + [Fact] + public void TestChainingArrowWorkerFunction() + { + var func1 = new ArrowWorkerFunction( + new ArrowUdfWrapper( + (numbers, strings) => + (StringArray)ToArrowArray( + Enumerable + .Range(0, strings.Length) + .Select(i => $"{strings.GetString(i)}:{numbers.Values[i]}") + .ToArray() + ) + ).Execute + ); + + var func2 = new ArrowWorkerFunction( + new ArrowUdfWrapper( + (strings) => + (StringArray)ToArrowArray( + Enumerable + .Range(0, strings.Length) + .Select(i => $"outer1:{strings.GetString(i)}") + .ToArray() + ) + ).Execute + ); + + var func3 = new ArrowWorkerFunction( + new ArrowUdfWrapper( + (strings) => + (StringArray)ToArrowArray( + Enumerable + .Range(0, strings.Length) + .Select(i => $"outer2:{strings.GetString(i)}") + .ToArray() + ) + ).Execute + ); + + var input = new IArrowArray[] { ToArrowArray(new[] { 100 }), ToArrowArray(new[] { "name" }) }; + + // Validate one-level chaining. + ArrowWorkerFunction chainedFunc1 = ArrowWorkerFunction.Chain(func1, func2); + AssertEquals("outer1:name:100", chainedFunc1.Func(input, new[] { 0, 1 })); + + // Validate two-level chaining. + ArrowWorkerFunction chainedFunc2 = ArrowWorkerFunction.Chain(chainedFunc1, func3); + AssertEquals("outer2:outer1:name:100", chainedFunc2.Func(input, new[] { 0, 1 })); + } + + [Fact] + public void TestChainingDataFrameWorkerFunction() + { + var func1 = new DataFrameWorkerFunction( + new DataFrameUdfWrapper< + Int32DataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >( + (numbers, strings) => + { + long i = 0; + return strings.Apply(cur => $"{cur}:{numbers[i++]}"); + } + ).Execute + ); + + var func2 = new DataFrameWorkerFunction( + new DataFrameUdfWrapper( + (strings) => strings.Apply(cur => $"outer1:{cur}") + ).Execute + ); + + var func3 = new DataFrameWorkerFunction( + new DataFrameUdfWrapper( + (strings) => strings.Apply(cur => $"outer2:{cur}") + ).Execute + ); + + string[] inputString = { "name" }; + var column = (StringArray)ToArrowArray(inputString); + ArrowStringDataFrameColumn ArrowStringDataFrameColumn = ToArrowStringDataFrameColumn(column); + var input = new DataFrameColumn[] + { + new Int32DataFrameColumn("Int", new List() { 100 }), + ArrowStringDataFrameColumn, + }; + + // Validate one-level chaining. + DataFrameWorkerFunction chainedFunc1 = DataFrameWorkerFunction.Chain(func1, func2); + ArrowTestUtils.AssertEquals("outer1:name:100", chainedFunc1.Func(input, new[] { 0, 1 })); + + // Validate two-level chaining. + DataFrameWorkerFunction chainedFunc2 = DataFrameWorkerFunction.Chain(chainedFunc1, func3); + ArrowTestUtils.AssertEquals( + "outer2:outer1:name:100", + chainedFunc2.Func(input, new[] { 0, 1 }) + ); + } + + [Fact] + public void TestInvalidChainingArrow() + { + var func1 = new ArrowWorkerFunction( + new ArrowUdfWrapper( + (numbers, strings) => + (StringArray)ToArrowArray( + Enumerable + .Range(0, strings.Length) + .Select(i => $"{strings.GetString(i)}:{numbers.Values[i]}") + .ToArray() + ) + ).Execute + ); + + var func2 = new ArrowWorkerFunction( + new ArrowUdfWrapper( + (strings) => + (StringArray)ToArrowArray( + Enumerable + .Range(0, strings.Length) + .Select(i => $"outer1:{strings.GetString(i)}") + .ToArray() + ) + ).Execute + ); + + IArrowArray[] input = new[] { ToArrowArray(new[] { 100 }), ToArrowArray(new[] { "name" }) }; + + // The order does not align since workerFunction2 is executed first. + ArrowWorkerFunction chainedFunc1 = ArrowWorkerFunction.Chain(func2, func1); + Assert.ThrowsAny(() => chainedFunc1.Func(input, new[] { 0, 1 })); + } + + [Fact] + public void TestInvalidChainingDataFrame() + { + var func1 = new DataFrameWorkerFunction( + new DataFrameUdfWrapper< + Int32DataFrameColumn, + ArrowStringDataFrameColumn, + ArrowStringDataFrameColumn + >( + (numbers, strings) => + { + long i = 0; + return strings.Apply(cur => $"{cur}:{numbers[i++]}"); + } + ).Execute + ); + + var func2 = new DataFrameWorkerFunction( + new DataFrameUdfWrapper( + (strings) => strings.Apply(cur => $"outer1:{cur}") + ).Execute + ); + + string[] inputString = { "name" }; + var column = (StringArray)ToArrowArray(inputString); + ArrowStringDataFrameColumn ArrowStringDataFrameColumn = ToArrowStringDataFrameColumn(column); + var input = new DataFrameColumn[] + { + new Int32DataFrameColumn("Int", new List() { 100 }), + ArrowStringDataFrameColumn, + }; + + // The order does not align since workerFunction2 is executed first. + DataFrameWorkerFunction chainedFunc1 = DataFrameWorkerFunction.Chain(func2, func1); + Assert.ThrowsAny(() => chainedFunc1.Func(input, new[] { 0, 1 })); + } + } +} diff --git a/tests/README.md b/tests/README.md index 7b83f8b8..3d465e82 100644 --- a/tests/README.md +++ b/tests/README.md @@ -110,36 +110,13 @@ nx run test/examples:test ### Coverage Collection -Run tests with coverage collection: -```bash -nx run flowthru:test:coverage -``` - -Generate HTML coverage report from collected data: -```bash -nx run flowthru:coverage:report -``` - -The report is written to `CoverageReport/index.html`. +Tests are run with coverage collection enabled in CI via `coverlet.runsettings`. Coverage reports are aggregated and tracked by [Codecov](https://codecov.io/gh/chaoticgoodcomputing/flowthru), with per-flag carryforward so partial `nx affected` runs don't erase unaffected projects. -Run the full CI pipeline (restore → build → test → report): +To force a clean test run by removing previous `TestResults` artifacts: ```bash -nx run flowthru:ci +nx run tests:purge ``` -### Convenience Target - -For local development, use the legacy test target with report configuration: -```bash -nx run flowthru:test --configuration=report -``` - -This target: -1. Purges previous coverage data -2. Runs all tests with coverage collection -3. Generates an HTML report -4. Opens the report in your browser - ### What Gets Measured Code coverage is collected for all `Flowthru*` assemblies: diff --git a/tests/project.json b/tests/project.json new file mode 100644 index 00000000..57b8e374 --- /dev/null +++ b/tests/project.json @@ -0,0 +1,19 @@ +{ + "name": "tests", + "$schema": "../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "tests", + "targets": { + "purge": { + "//": "Remove all TestResults directories to force a clean test run", + "executor": "nx:run-commands", + "options": { + "commands": [ + "find tests -name TestResults -type d -exec rm -rf {} + 2>/dev/null || true" + ], + "cwd": "{workspaceRoot}", + "parallel": false + } + } + } +}